aboutsummaryrefslogtreecommitdiffhomepage
path: root/Firebase
diff options
context:
space:
mode:
authorGravatar Paul Beusterien <paulbeusterien@google.com>2017-05-15 12:27:07 -0700
committerGravatar Paul Beusterien <paulbeusterien@google.com>2017-05-15 12:27:07 -0700
commit98ba64449a632518bd2b86fe8d927f4a960d3ddc (patch)
tree131d9c4272fa6179fcda6c5a33fcb3b1bd57ad2e /Firebase
parent32461366c9e204a527ca05e6e9b9404a2454ac51 (diff)
Initial
Diffstat (limited to 'Firebase')
-rw-r--r--Firebase/Auth/CHANGELOG.md62
-rw-r--r--Firebase/Auth/Docs/threading.md119
-rw-r--r--Firebase/Auth/FirebaseAuth.podspec56
-rw-r--r--Firebase/Auth/README.md8
-rw-r--r--Firebase/Auth/Source/AuthProviders/EmailPassword/FIREmailAuthProvider.h63
-rw-r--r--Firebase/Auth/Source/AuthProviders/EmailPassword/FIREmailAuthProvider.m35
-rw-r--r--Firebase/Auth/Source/AuthProviders/EmailPassword/FIREmailPasswordAuthCredential.h48
-rw-r--r--Firebase/Auth/Source/AuthProviders/EmailPassword/FIREmailPasswordAuthCredential.m51
-rw-r--r--Firebase/Auth/Source/AuthProviders/EmailPassword/FIREmailPasswordAuthProvider.h49
-rw-r--r--Firebase/Auth/Source/AuthProviders/EmailPassword/FIREmailPasswordAuthProvider.m35
-rw-r--r--Firebase/Auth/Source/AuthProviders/Facebook/FIRFacebookAuthCredential.h36
-rw-r--r--Firebase/Auth/Source/AuthProviders/Facebook/FIRFacebookAuthCredential.m51
-rw-r--r--Firebase/Auth/Source/AuthProviders/Facebook/FIRFacebookAuthProvider.h51
-rw-r--r--Firebase/Auth/Source/AuthProviders/Facebook/FIRFacebookAuthProvider.m36
-rw-r--r--Firebase/Auth/Source/AuthProviders/GitHub/FIRGitHubAuthCredential.h41
-rw-r--r--Firebase/Auth/Source/AuthProviders/GitHub/FIRGitHubAuthCredential.m49
-rw-r--r--Firebase/Auth/Source/AuthProviders/GitHub/FIRGitHubAuthProvider.h51
-rw-r--r--Firebase/Auth/Source/AuthProviders/GitHub/FIRGitHubAuthProvider.m36
-rw-r--r--Firebase/Auth/Source/AuthProviders/Google/FIRGoogleAuthCredential.h38
-rw-r--r--Firebase/Auth/Source/AuthProviders/Google/FIRGoogleAuthCredential.m54
-rw-r--r--Firebase/Auth/Source/AuthProviders/Google/FIRGoogleAuthProvider.h53
-rw-r--r--Firebase/Auth/Source/AuthProviders/Google/FIRGoogleAuthProvider.m37
-rw-r--r--Firebase/Auth/Source/AuthProviders/OAuth/FIROAuthCredential.h55
-rw-r--r--Firebase/Auth/Source/AuthProviders/OAuth/FIROAuthCredential.m50
-rw-r--r--Firebase/Auth/Source/AuthProviders/OAuth/FIROAuthProvider.h64
-rw-r--r--Firebase/Auth/Source/AuthProviders/OAuth/FIROAuthProvider.m42
-rw-r--r--Firebase/Auth/Source/AuthProviders/Phone/FIRPhoneAuthCredential.h37
-rw-r--r--Firebase/Auth/Source/AuthProviders/Phone/FIRPhoneAuthCredential.m65
-rw-r--r--Firebase/Auth/Source/AuthProviders/Phone/FIRPhoneAuthCredential_Internal.h70
-rw-r--r--Firebase/Auth/Source/AuthProviders/Phone/FIRPhoneAuthProvider.h90
-rw-r--r--Firebase/Auth/Source/AuthProviders/Phone/FIRPhoneAuthProvider.m213
-rw-r--r--Firebase/Auth/Source/AuthProviders/Phone/NSString+FIRAuth.h36
-rw-r--r--Firebase/Auth/Source/AuthProviders/Phone/NSString+FIRAuth.m36
-rw-r--r--Firebase/Auth/Source/AuthProviders/Twitter/FIRTwitterAuthCredential.h48
-rw-r--r--Firebase/Auth/Source/AuthProviders/Twitter/FIRTwitterAuthCredential.m51
-rw-r--r--Firebase/Auth/Source/AuthProviders/Twitter/FIRTwitterAuthProvider.h52
-rw-r--r--Firebase/Auth/Source/AuthProviders/Twitter/FIRTwitterAuthProvider.m36
-rw-r--r--Firebase/Auth/Source/FIRActionCodeSettings.m39
-rw-r--r--Firebase/Auth/Source/FIRAdditionalUserInfo.h59
-rw-r--r--Firebase/Auth/Source/FIRAdditionalUserInfo.m98
-rw-r--r--Firebase/Auth/Source/FIRAuth.h612
-rw-r--r--Firebase/Auth/Source/FIRAuth.m1252
-rw-r--r--Firebase/Auth/Source/FIRAuthAPNSToken.m34
-rw-r--r--Firebase/Auth/Source/FIRAuthAPNSTokenManager.m255
-rw-r--r--Firebase/Auth/Source/FIRAuthAPNSTokenType.h42
-rw-r--r--Firebase/Auth/Source/FIRAuthAppCredential.m64
-rw-r--r--Firebase/Auth/Source/FIRAuthAppCredentialManager.m164
-rw-r--r--Firebase/Auth/Source/FIRAuthAppDelegateProxy.m245
-rw-r--r--Firebase/Auth/Source/FIRAuthCredential.h43
-rw-r--r--Firebase/Auth/Source/FIRAuthCredential.m42
-rw-r--r--Firebase/Auth/Source/FIRAuthDataResult.h51
-rw-r--r--Firebase/Auth/Source/FIRAuthDataResult.m69
-rw-r--r--Firebase/Auth/Source/FIRAuthDispatcher.m46
-rw-r--r--Firebase/Auth/Source/FIRAuthErrorUtils.m794
-rw-r--r--Firebase/Auth/Source/FIRAuthErrors.h258
-rw-r--r--Firebase/Auth/Source/FIRAuthExceptionUtils.h41
-rw-r--r--Firebase/Auth/Source/FIRAuthExceptionUtils.m36
-rw-r--r--Firebase/Auth/Source/FIRAuthGlobalWorkQueue.m26
-rw-r--r--Firebase/Auth/Source/FIRAuthKeychain.m256
-rw-r--r--Firebase/Auth/Source/FIRAuthNotificationManager.m175
-rw-r--r--Firebase/Auth/Source/FIRAuthProvider.m38
-rw-r--r--Firebase/Auth/Source/FIRAuthSerialTaskQueue.m52
-rw-r--r--Firebase/Auth/Source/FIRAuthSwiftNameSupport.h29
-rw-r--r--Firebase/Auth/Source/FIRAuthUserDefaultsStorage.m78
-rw-r--r--Firebase/Auth/Source/FIRSecureTokenService.h96
-rw-r--r--Firebase/Auth/Source/FIRSecureTokenService.m214
-rw-r--r--Firebase/Auth/Source/FIRUser.h463
-rw-r--r--Firebase/Auth/Source/FIRUser.m1170
-rw-r--r--Firebase/Auth/Source/FIRUserInfo.h62
-rw-r--r--Firebase/Auth/Source/FIRUserInfoImpl.h61
-rw-r--r--Firebase/Auth/Source/FIRUserInfoImpl.m127
-rw-r--r--Firebase/Auth/Source/FirebaseAuth.h27
-rwxr-xr-xFirebase/Auth/Source/FirebaseAuthVersion.h27
-rw-r--r--Firebase/Auth/Source/FirebaseAuthVersion.m26
-rw-r--r--Firebase/Auth/Source/Private/FIRActionCodeSettings.h91
-rw-r--r--Firebase/Auth/Source/Private/FIRAdditionalUserInfo_Internal.h46
-rw-r--r--Firebase/Auth/Source/Private/FIRAuthAPNSToken.h54
-rw-r--r--Firebase/Auth/Source/Private/FIRAuthAPNSTokenManager.h69
-rw-r--r--Firebase/Auth/Source/Private/FIRAuthAppCredential.h53
-rw-r--r--Firebase/Auth/Source/Private/FIRAuthAppCredentialManager.h85
-rw-r--r--Firebase/Auth/Source/Private/FIRAuthAppDelegateProxy.h74
-rw-r--r--Firebase/Auth/Source/Private/FIRAuthCredential_Internal.h41
-rw-r--r--Firebase/Auth/Source/Private/FIRAuthDataResult_Internal.h34
-rw-r--r--Firebase/Auth/Source/Private/FIRAuthDispatcher.h63
-rw-r--r--Firebase/Auth/Source/Private/FIRAuthErrorUtils.h418
-rw-r--r--Firebase/Auth/Source/Private/FIRAuthGlobalWorkQueue.h31
-rw-r--r--Firebase/Auth/Source/Private/FIRAuthInternalErrors.h365
-rw-r--r--Firebase/Auth/Source/Private/FIRAuthKeychain.h70
-rw-r--r--Firebase/Auth/Source/Private/FIRAuthNotificationManager.h71
-rw-r--r--Firebase/Auth/Source/Private/FIRAuthSerialTaskQueue.h50
-rw-r--r--Firebase/Auth/Source/Private/FIRAuthUserDefaultsStorage.h47
-rw-r--r--Firebase/Auth/Source/Private/FIRAuth_Internal.h123
-rw-r--r--Firebase/Auth/Source/Private/FIRUser_Internal.h80
-rw-r--r--Firebase/Auth/Source/RPCs/FIRAuthBackend.h496
-rw-r--r--Firebase/Auth/Source/RPCs/FIRAuthBackend.m943
-rw-r--r--Firebase/Auth/Source/RPCs/FIRAuthRPCRequest.h40
-rw-r--r--Firebase/Auth/Source/RPCs/FIRAuthRPCResponse.h49
-rw-r--r--Firebase/Auth/Source/RPCs/FIRCreateAuthURIRequest.h86
-rw-r--r--Firebase/Auth/Source/RPCs/FIRCreateAuthURIRequest.m95
-rw-r--r--Firebase/Auth/Source/RPCs/FIRCreateAuthURIResponse.h56
-rw-r--r--Firebase/Auth/Source/RPCs/FIRCreateAuthURIResponse.m33
-rw-r--r--Firebase/Auth/Source/RPCs/FIRDeleteAccountRequest.h48
-rw-r--r--Firebase/Auth/Source/RPCs/FIRDeleteAccountRequest.m65
-rw-r--r--Firebase/Auth/Source/RPCs/FIRDeleteAccountResponse.h26
-rw-r--r--Firebase/Auth/Source/RPCs/FIRDeleteAccountResponse.m28
-rw-r--r--Firebase/Auth/Source/RPCs/FIRGetAccountInfoRequest.h51
-rw-r--r--Firebase/Auth/Source/RPCs/FIRGetAccountInfoRequest.m47
-rw-r--r--Firebase/Auth/Source/RPCs/FIRGetAccountInfoResponse.h146
-rw-r--r--Firebase/Auth/Source/RPCs/FIRGetAccountInfoResponse.m94
-rw-r--r--Firebase/Auth/Source/RPCs/FIRGetOOBConfirmationCodeRequest.h87
-rw-r--r--Firebase/Auth/Source/RPCs/FIRGetOOBConfirmationCodeRequest.m135
-rw-r--r--Firebase/Auth/Source/RPCs/FIRGetOOBConfirmationCodeResponse.h35
-rw-r--r--Firebase/Auth/Source/RPCs/FIRGetOOBConfirmationCodeResponse.m38
-rw-r--r--Firebase/Auth/Source/RPCs/FIRIdentityToolkitRequest.h57
-rw-r--r--Firebase/Auth/Source/RPCs/FIRIdentityToolkitRequest.m57
-rw-r--r--Firebase/Auth/Source/RPCs/FIRResetPasswordRequest.h53
-rw-r--r--Firebase/Auth/Source/RPCs/FIRResetPasswordRequest.m56
-rw-r--r--Firebase/Auth/Source/RPCs/FIRResetPasswordResponse.h52
-rw-r--r--Firebase/Auth/Source/RPCs/FIRResetPasswordResponse.m31
-rw-r--r--Firebase/Auth/Source/RPCs/FIRSecureTokenRequest.h109
-rw-r--r--Firebase/Auth/Source/RPCs/FIRSecureTokenRequest.m141
-rw-r--r--Firebase/Auth/Source/RPCs/FIRSecureTokenResponse.h50
-rw-r--r--Firebase/Auth/Source/RPCs/FIRSecureTokenResponse.m70
-rw-r--r--Firebase/Auth/Source/RPCs/FIRSendVerificationCodeRequest.h56
-rw-r--r--Firebase/Auth/Source/RPCs/FIRSendVerificationCodeRequest.m73
-rw-r--r--Firebase/Auth/Source/RPCs/FIRSendVerificationCodeResponse.h32
-rw-r--r--Firebase/Auth/Source/RPCs/FIRSendVerificationCodeResponse.m36
-rw-r--r--Firebase/Auth/Source/RPCs/FIRSetAccountInfoRequest.h149
-rw-r--r--Firebase/Auth/Source/RPCs/FIRSetAccountInfoRequest.m174
-rw-r--r--Firebase/Auth/Source/RPCs/FIRSetAccountInfoResponse.h98
-rw-r--r--Firebase/Auth/Source/RPCs/FIRSetAccountInfoResponse.m61
-rw-r--r--Firebase/Auth/Source/RPCs/FIRSignUpNewUserRequest.h67
-rw-r--r--Firebase/Auth/Source/RPCs/FIRSignUpNewUserRequest.m82
-rw-r--r--Firebase/Auth/Source/RPCs/FIRSignUpNewUserResponse.h44
-rw-r--r--Firebase/Auth/Source/RPCs/FIRSignUpNewUserResponse.m32
-rw-r--r--Firebase/Auth/Source/RPCs/FIRVerifyAssertionRequest.h100
-rw-r--r--Firebase/Auth/Source/RPCs/FIRVerifyAssertionRequest.m142
-rw-r--r--Firebase/Auth/Source/RPCs/FIRVerifyAssertionResponse.h186
-rw-r--r--Firebase/Auth/Source/RPCs/FIRVerifyAssertionResponse.m78
-rw-r--r--Firebase/Auth/Source/RPCs/FIRVerifyClientRequest.h53
-rw-r--r--Firebase/Auth/Source/RPCs/FIRVerifyClientRequest.m63
-rw-r--r--Firebase/Auth/Source/RPCs/FIRVerifyClientResponse.h38
-rw-r--r--Firebase/Auth/Source/RPCs/FIRVerifyClientResponse.m33
-rw-r--r--Firebase/Auth/Source/RPCs/FIRVerifyCustomTokenRequest.h56
-rw-r--r--Firebase/Auth/Source/RPCs/FIRVerifyCustomTokenRequest.m56
-rw-r--r--Firebase/Auth/Source/RPCs/FIRVerifyCustomTokenResponse.h47
-rw-r--r--Firebase/Auth/Source/RPCs/FIRVerifyCustomTokenResponse.m32
-rw-r--r--Firebase/Auth/Source/RPCs/FIRVerifyPasswordRequest.h79
-rw-r--r--Firebase/Auth/Source/RPCs/FIRVerifyPasswordRequest.m91
-rw-r--r--Firebase/Auth/Source/RPCs/FIRVerifyPasswordResponse.h72
-rw-r--r--Firebase/Auth/Source/RPCs/FIRVerifyPasswordResponse.m36
-rw-r--r--Firebase/Auth/Source/RPCs/FIRVerifyPhoneNumberRequest.h78
-rw-r--r--Firebase/Auth/Source/RPCs/FIRVerifyPhoneNumberRequest.m97
-rw-r--r--Firebase/Auth/Source/RPCs/FIRVerifyPhoneNumberResponse.h64
-rw-r--r--Firebase/Auth/Source/RPCs/FIRVerifyPhoneNumberResponse.m42
-rw-r--r--Firebase/Core/FIRAnalyticsConfiguration.h54
-rw-r--r--Firebase/Core/FIRAnalyticsConfiguration.m61
-rw-r--r--Firebase/Core/FIRApp.h126
-rw-r--r--Firebase/Core/FIRApp.m596
-rw-r--r--Firebase/Core/FIRAppAssociationRegistration.m47
-rw-r--r--Firebase/Core/FIRAppEnvironmentUtil.m207
-rw-r--r--Firebase/Core/FIRBundleUtil.m65
-rw-r--r--Firebase/Core/FIRConfiguration.h80
-rw-r--r--Firebase/Core/FIRConfiguration.m51
-rw-r--r--Firebase/Core/FIRCoreSwiftNameSupport.h29
-rw-r--r--Firebase/Core/FIRErrors.m33
-rw-r--r--Firebase/Core/FIRLogger.m228
-rw-r--r--Firebase/Core/FIRLoggerLevel.h30
-rw-r--r--Firebase/Core/FIRMutableDictionary.m97
-rw-r--r--Firebase/Core/FIRNetwork.m390
-rw-r--r--Firebase/Core/FIRNetworkConstants.m39
-rw-r--r--Firebase/Core/FIRNetworkURLSession.m669
-rw-r--r--Firebase/Core/FIROptions.h131
-rw-r--r--Firebase/Core/FIROptions.m427
-rw-r--r--Firebase/Core/FIRReachabilityChecker.m245
-rw-r--r--Firebase/Core/FIRURLSchemeUtil.m43
-rw-r--r--Firebase/Core/FirebaseCore.h21
-rw-r--r--Firebase/Core/FirebaseCore.podspec35
-rw-r--r--Firebase/Core/Private/FIRAnalyticsConfiguration+Internal.h39
-rw-r--r--Firebase/Core/Private/FIRAppAssociationRegistration.h48
-rw-r--r--Firebase/Core/Private/FIRAppEnvironmentUtil.h48
-rw-r--r--Firebase/Core/Private/FIRAppInternal.h140
-rw-r--r--Firebase/Core/Private/FIRBundleUtil.h57
-rw-r--r--Firebase/Core/Private/FIRErrorCode.h55
-rw-r--r--Firebase/Core/Private/FIRErrors.h43
-rw-r--r--Firebase/Core/Private/FIRLogger.h115
-rw-r--r--Firebase/Core/Private/FIRMutableDictionary.h46
-rw-r--r--Firebase/Core/Private/FIRNetwork.h87
-rw-r--r--Firebase/Core/Private/FIRNetworkConstants.h75
-rw-r--r--Firebase/Core/Private/FIRNetworkLoggerProtocol.h50
-rw-r--r--Firebase/Core/Private/FIRNetworkMessageCode.h52
-rw-r--r--Firebase/Core/Private/FIRNetworkURLSession.h57
-rw-r--r--Firebase/Core/Private/FIROptionsInternal.h108
-rw-r--r--Firebase/Core/Private/FIRReachabilityChecker+Internal.h47
-rw-r--r--Firebase/Core/Private/FIRReachabilityChecker.h83
-rw-r--r--Firebase/Core/Private/FIRURLSchemeUtil.h25
-rw-r--r--Firebase/Database/Api/FIRDataEventType.h39
-rw-r--r--Firebase/Database/Api/FIRDataSnapshot.h148
-rw-r--r--Firebase/Database/Api/FIRDataSnapshot.m101
-rw-r--r--Firebase/Database/Api/FIRDatabase.h140
-rw-r--r--Firebase/Database/Api/FIRDatabase.m268
-rw-r--r--Firebase/Database/Api/FIRDatabaseConfig.h63
-rw-r--r--Firebase/Database/Api/FIRDatabaseConfig.m117
-rw-r--r--Firebase/Database/Api/FIRDatabaseQuery.h315
-rw-r--r--Firebase/Database/Api/FIRDatabaseQuery.m525
-rw-r--r--Firebase/Database/Api/FIRDatabaseSwiftNameSupport.h29
-rw-r--r--Firebase/Database/Api/FIRMutableData.h130
-rw-r--r--Firebase/Database/Api/FIRMutableData.m134
-rw-r--r--Firebase/Database/Api/FIRServerValue.h35
-rw-r--r--Firebase/Database/Api/FIRServerValue.m30
-rw-r--r--Firebase/Database/Api/FIRTransactionResult.h47
-rw-r--r--Firebase/Database/Api/FIRTransactionResult.m39
-rw-r--r--Firebase/Database/Api/FirebaseDatabase.h29
-rw-r--r--Firebase/Database/Api/Private/FIRDataSnapshot_Private.h27
-rw-r--r--Firebase/Database/Api/Private/FIRDatabaseQuery_Private.h43
-rw-r--r--Firebase/Database/Api/Private/FIRDatabaseReference_Private.h29
-rw-r--r--Firebase/Database/Api/Private/FIRDatabase_Private.h28
-rw-r--r--Firebase/Database/Api/Private/FIRMutableData_Private.h26
-rw-r--r--Firebase/Database/Api/Private/FIRTransactionResult_Private.h25
-rw-r--r--Firebase/Database/Api/Private/FTypedefs_Private.h56
-rw-r--r--Firebase/Database/Constants/FConstants.h190
-rw-r--r--Firebase/Database/Constants/FConstants.m183
-rw-r--r--Firebase/Database/Core/FCompoundHash.h40
-rw-r--r--Firebase/Database/Core/FCompoundHash.m236
-rw-r--r--Firebase/Database/Core/FListenProvider.h33
-rw-r--r--Firebase/Database/Core/FListenProvider.m26
-rw-r--r--Firebase/Database/Core/FPersistentConnection.h78
-rw-r--r--Firebase/Database/Core/FPersistentConnection.m945
-rw-r--r--Firebase/Database/Core/FQueryParams.h59
-rw-r--r--Firebase/Database/Core/FQueryParams.m372
-rw-r--r--Firebase/Database/Core/FQuerySpec.h36
-rw-r--r--Firebase/Database/Core/FQuerySpec.m85
-rw-r--r--Firebase/Database/Core/FRangeMerge.h35
-rw-r--r--Firebase/Database/Core/FRangeMerge.m107
-rw-r--r--Firebase/Database/Core/FRepo.h76
-rw-r--r--Firebase/Database/Core/FRepo.m1116
-rw-r--r--Firebase/Database/Core/FRepoInfo.h34
-rw-r--r--Firebase/Database/Core/FRepoInfo.m115
-rw-r--r--Firebase/Database/Core/FRepoManager.h32
-rw-r--r--Firebase/Database/Core/FRepoManager.m131
-rw-r--r--Firebase/Database/Core/FRepo_Private.h42
-rw-r--r--Firebase/Database/Core/FServerValues.h30
-rw-r--r--Firebase/Database/Core/FServerValues.m93
-rw-r--r--Firebase/Database/Core/FSnapshotHolder.h27
-rw-r--r--Firebase/Database/Core/FSnapshotHolder.m46
-rw-r--r--Firebase/Database/Core/FSparseSnapshotTree.h34
-rw-r--r--Firebase/Database/Core/FSparseSnapshotTree.m144
-rw-r--r--Firebase/Database/Core/FSyncPoint.h66
-rw-r--r--Firebase/Database/Core/FSyncPoint.m257
-rw-r--r--Firebase/Database/Core/FSyncTree.h61
-rw-r--r--Firebase/Database/Core/FSyncTree.m817
-rw-r--r--Firebase/Database/Core/FWriteRecord.h40
-rw-r--r--Firebase/Database/Core/FWriteRecord.m117
-rw-r--r--Firebase/Database/Core/FWriteTree.h63
-rw-r--r--Firebase/Database/Core/FWriteTree.m458
-rw-r--r--Firebase/Database/Core/FWriteTreeRef.h51
-rw-r--r--Firebase/Database/Core/FWriteTreeRef.m133
-rw-r--r--Firebase/Database/Core/Operation/FAckUserWrite.h35
-rw-r--r--Firebase/Database/Core/Operation/FAckUserWrite.m55
-rw-r--r--Firebase/Database/Core/Operation/FMerge.h30
-rw-r--r--Firebase/Database/Core/Operation/FMerge.m71
-rw-r--r--Firebase/Database/Core/Operation/FOperation.h34
-rw-r--r--Firebase/Database/Core/Operation/FOperationSource.h34
-rw-r--r--Firebase/Database/Core/Operation/FOperationSource.m73
-rw-r--r--Firebase/Database/Core/Operation/FOverwrite.h30
-rw-r--r--Firebase/Database/Core/Operation/FOverwrite.m62
-rw-r--r--Firebase/Database/Core/Utilities/FIRRetryHelper.h33
-rw-r--r--Firebase/Database/Core/Utilities/FIRRetryHelper.m139
-rw-r--r--Firebase/Database/Core/Utilities/FImmutableTree.h51
-rw-r--r--Firebase/Database/Core/Utilities/FImmutableTree.m421
-rw-r--r--Firebase/Database/Core/Utilities/FPath.h45
-rw-r--r--Firebase/Database/Core/Utilities/FPath.m298
-rw-r--r--Firebase/Database/Core/Utilities/FTree.h48
-rw-r--r--Firebase/Database/Core/Utilities/FTree.m183
-rw-r--r--Firebase/Database/Core/Utilities/FTreeNode.h25
-rw-r--r--Firebase/Database/Core/Utilities/FTreeNode.m36
-rw-r--r--Firebase/Database/Core/View/FCacheNode.h44
-rw-r--r--Firebase/Database/Core/View/FCacheNode.m60
-rw-r--r--Firebase/Database/Core/View/FCancelEvent.h30
-rw-r--r--Firebase/Database/Core/View/FCancelEvent.m55
-rw-r--r--Firebase/Database/Core/View/FChange.h38
-rw-r--r--Firebase/Database/Core/View/FChange.m65
-rw-r--r--Firebase/Database/Core/View/FChildEventRegistration.h37
-rw-r--r--Firebase/Database/Core/View/FChildEventRegistration.m92
-rw-r--r--Firebase/Database/Core/View/FDataEvent.h39
-rw-r--r--Firebase/Database/Core/View/FDataEvent.m74
-rw-r--r--Firebase/Database/Core/View/FEvent.h27
-rw-r--r--Firebase/Database/Core/View/FEventRaiser.h35
-rw-r--r--Firebase/Database/Core/View/FEventRaiser.m72
-rw-r--r--Firebase/Database/Core/View/FEventRegistration.h36
-rw-r--r--Firebase/Database/Core/View/FKeepSyncedEventRegistration.h28
-rw-r--r--Firebase/Database/Core/View/FKeepSyncedEventRegistration.m64
-rw-r--r--Firebase/Database/Core/View/FValueEventRegistration.h34
-rw-r--r--Firebase/Database/Core/View/FValueEventRegistration.m89
-rw-r--r--Firebase/Database/Core/View/FView.h53
-rw-r--r--Firebase/Database/Core/View/FView.m223
-rw-r--r--Firebase/Database/Core/View/FViewCache.h35
-rw-r--r--Firebase/Database/Core/View/FViewCache.m61
-rw-r--r--Firebase/Database/Core/View/Filter/FChildChangeAccumulator.h28
-rw-r--r--Firebase/Database/Core/View/Filter/FChildChangeAccumulator.m80
-rw-r--r--Firebase/Database/Core/View/Filter/FCompleteChildSource.h28
-rw-r--r--Firebase/Database/Core/View/Filter/FIndexedFilter.h27
-rw-r--r--Firebase/Database/Core/View/Filter/FIndexedFilter.m147
-rw-r--r--Firebase/Database/Core/View/Filter/FLimitedFilter.h26
-rw-r--r--Firebase/Database/Core/View/Filter/FLimitedFilter.m243
-rw-r--r--Firebase/Database/Core/View/Filter/FNodeFilter.h71
-rw-r--r--Firebase/Database/FClock.h35
-rw-r--r--Firebase/Database/FClock.m58
-rw-r--r--Firebase/Database/FEventGenerator.h27
-rw-r--r--Firebase/Database/FEventGenerator.m141
-rw-r--r--Firebase/Database/FIRDatabaseConfig_Private.h35
-rw-r--r--Firebase/Database/FIRDatabaseReference.h719
-rw-r--r--Firebase/Database/FIRDatabaseReference.m404
-rw-r--r--Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary.xcodeproj/project.pbxproj438
-rw-r--r--Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FArraySortedDictionary.h37
-rw-r--r--Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FArraySortedDictionary.m282
-rw-r--r--Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FImmutableSortedDictionary-Prefix.pch7
-rw-r--r--Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FImmutableSortedDictionary.h71
-rw-r--r--Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FImmutableSortedDictionary.m158
-rw-r--r--Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FImmutableSortedSet.h38
-rw-r--r--Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FImmutableSortedSet.m131
-rw-r--r--Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FLLRBEmptyNode.h43
-rw-r--r--Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FLLRBEmptyNode.m87
-rw-r--r--Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FLLRBNode.h45
-rw-r--r--Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FLLRBValueNode.h45
-rw-r--r--Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FLLRBValueNode.m245
-rw-r--r--Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FTreeSortedDictionaryEnumerator.h25
-rw-r--r--Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FTreeSortedDictionaryEnumerator.m99
-rw-r--r--Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionaryTests/FImmutableSortedDictionaryTests-Info.plist22
-rw-r--r--Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionaryTests/en.lproj/InfoPlist.strings2
-rw-r--r--Firebase/Database/FIndex.h50
-rw-r--r--Firebase/Database/FIndex.m38
-rw-r--r--Firebase/Database/FKeyIndex.h23
-rw-r--r--Firebase/Database/FKeyIndex.m115
-rw-r--r--Firebase/Database/FListenComplete.h29
-rw-r--r--Firebase/Database/FListenComplete.m51
-rw-r--r--Firebase/Database/FMaxNode.h23
-rw-r--r--Firebase/Database/FMaxNode.m61
-rw-r--r--Firebase/Database/FNamedNode.h32
-rw-r--r--Firebase/Database/FNamedNode.m94
-rw-r--r--Firebase/Database/FPathIndex.h23
-rw-r--r--Firebase/Database/FPathIndex.m125
-rw-r--r--Firebase/Database/FPriorityIndex.h23
-rw-r--r--Firebase/Database/FPriorityIndex.m118
-rw-r--r--Firebase/Database/FRangedFilter.h32
-rw-r--r--Firebase/Database/FRangedFilter.m118
-rw-r--r--Firebase/Database/FTransformedEnumerator.h24
-rw-r--r--Firebase/Database/FTransformedEnumerator.m43
-rw-r--r--Firebase/Database/FTreeSortedDictionary.h46
-rw-r--r--Firebase/Database/FTreeSortedDictionary.m342
-rw-r--r--Firebase/Database/FValueIndex.h23
-rw-r--r--Firebase/Database/FValueIndex.m106
-rw-r--r--Firebase/Database/FViewProcessor.h41
-rw-r--r--Firebase/Database/FViewProcessor.m654
-rw-r--r--Firebase/Database/FViewProcessorResult.h30
-rw-r--r--Firebase/Database/FViewProcessorResult.m35
-rw-r--r--Firebase/Database/Firebase-Prefix.pch7
-rw-r--r--Firebase/Database/FirebaseDatabase.podspec48
-rw-r--r--Firebase/Database/Info.plist26
-rw-r--r--Firebase/Database/Login/FAuthTokenProvider.h36
-rw-r--r--Firebase/Database/Login/FAuthTokenProvider.m162
-rw-r--r--Firebase/Database/Login/FIRNoopAuthTokenProvider.h22
-rw-r--r--Firebase/Database/Login/FIRNoopAuthTokenProvider.m33
-rw-r--r--Firebase/Database/Persistence/FCachePolicy.h41
-rw-r--r--Firebase/Database/Persistence/FCachePolicy.m79
-rw-r--r--Firebase/Database/Persistence/FLevelDBStorageEngine.h37
-rw-r--r--Firebase/Database/Persistence/FLevelDBStorageEngine.m717
-rw-r--r--Firebase/Database/Persistence/FPendingPut.h55
-rw-r--r--Firebase/Database/Persistence/FPendingPut.m112
-rw-r--r--Firebase/Database/Persistence/FPersistenceManager.h52
-rw-r--r--Firebase/Database/Persistence/FPersistenceManager.m190
-rw-r--r--Firebase/Database/Persistence/FPruneForest.h38
-rw-r--r--Firebase/Database/Persistence/FPruneForest.m177
-rw-r--r--Firebase/Database/Persistence/FStorageEngine.h53
-rw-r--r--Firebase/Database/Persistence/FTrackedQuery.h40
-rw-r--r--Firebase/Database/Persistence/FTrackedQuery.m102
-rw-r--r--Firebase/Database/Persistence/FTrackedQueryManager.h51
-rw-r--r--Firebase/Database/Persistence/FTrackedQueryManager.m321
-rw-r--r--Firebase/Database/Realtime/FConnection.h52
-rw-r--r--Firebase/Database/Realtime/FConnection.m211
-rw-r--r--Firebase/Database/Realtime/FWebSocketConnection.h46
-rw-r--r--Firebase/Database/Realtime/FWebSocketConnection.m305
-rw-r--r--Firebase/Database/Snapshot/FChildrenNode.h40
-rw-r--r--Firebase/Database/Snapshot/FChildrenNode.m385
-rw-r--r--Firebase/Database/Snapshot/FCompoundWrite.h61
-rw-r--r--Firebase/Database/Snapshot/FCompoundWrite.m257
-rw-r--r--Firebase/Database/Snapshot/FEmptyNode.h24
-rw-r--r--Firebase/Database/Snapshot/FEmptyNode.m29
-rw-r--r--Firebase/Database/Snapshot/FIndexedNode.h49
-rw-r--r--Firebase/Database/Snapshot/FIndexedNode.m202
-rw-r--r--Firebase/Database/Snapshot/FLeafNode.h28
-rw-r--r--Firebase/Database/Snapshot/FLeafNode.m250
-rw-r--r--Firebase/Database/Snapshot/FNode.h46
-rw-r--r--Firebase/Database/Snapshot/FSnapshotUtilities.h45
-rw-r--r--Firebase/Database/Snapshot/FSnapshotUtilities.m301
-rw-r--r--Firebase/Database/Utilities/FAtomicNumber.h23
-rw-r--r--Firebase/Database/Utilities/FAtomicNumber.m54
-rw-r--r--Firebase/Database/Utilities/FEventEmitter.h33
-rw-r--r--Firebase/Database/Utilities/FEventEmitter.m145
-rw-r--r--Firebase/Database/Utilities/FNextPushId.h23
-rw-r--r--Firebase/Database/Utilities/FNextPushId.m63
-rw-r--r--Firebase/Database/Utilities/FParsedUrl.h25
-rw-r--r--Firebase/Database/Utilities/FParsedUrl.m24
-rw-r--r--Firebase/Database/Utilities/FStringUtilities.h26
-rw-r--r--Firebase/Database/Utilities/FStringUtilities.m61
-rw-r--r--Firebase/Database/Utilities/FTypedefs.h45
-rw-r--r--Firebase/Database/Utilities/FUtilities.h76
-rw-r--r--Firebase/Database/Utilities/FUtilities.m389
-rw-r--r--Firebase/Database/Utilities/FValidation.h45
-rw-r--r--Firebase/Database/Utilities/FValidation.m312
-rw-r--r--Firebase/Database/Utilities/NSString+FURLUtils.h24
-rw-r--r--Firebase/Database/Utilities/NSString+FURLUtils.m38
-rw-r--r--Firebase/Database/Utilities/Tuples/FTupleBoolBlock.h25
-rw-r--r--Firebase/Database/Utilities/Tuples/FTupleBoolBlock.m24
-rw-r--r--Firebase/Database/Utilities/Tuples/FTupleCallbackStatus.h24
-rw-r--r--Firebase/Database/Utilities/Tuples/FTupleCallbackStatus.m22
-rw-r--r--Firebase/Database/Utilities/Tuples/FTupleFirebase.h26
-rw-r--r--Firebase/Database/Utilities/Tuples/FTupleFirebase.m25
-rw-r--r--Firebase/Database/Utilities/Tuples/FTupleNodePath.h28
-rw-r--r--Firebase/Database/Utilities/Tuples/FTupleNodePath.m33
-rw-r--r--Firebase/Database/Utilities/Tuples/FTupleObjectNode.h27
-rw-r--r--Firebase/Database/Utilities/Tuples/FTupleObjectNode.m32
-rw-r--r--Firebase/Database/Utilities/Tuples/FTupleObjects.h24
-rw-r--r--Firebase/Database/Utilities/Tuples/FTupleObjects.m24
-rw-r--r--Firebase/Database/Utilities/Tuples/FTupleOnDisconnect.h27
-rw-r--r--Firebase/Database/Utilities/Tuples/FTupleOnDisconnect.m26
-rw-r--r--Firebase/Database/Utilities/Tuples/FTuplePathValue.h25
-rw-r--r--Firebase/Database/Utilities/Tuples/FTuplePathValue.m38
-rw-r--r--Firebase/Database/Utilities/Tuples/FTupleRemovedQueriesEvents.h30
-rw-r--r--Firebase/Database/Utilities/Tuples/FTupleRemovedQueriesEvents.m37
-rw-r--r--Firebase/Database/Utilities/Tuples/FTupleSetIdPath.h27
-rw-r--r--Firebase/Database/Utilities/Tuples/FTupleSetIdPath.m33
-rw-r--r--Firebase/Database/Utilities/Tuples/FTupleStringNode.h27
-rw-r--r--Firebase/Database/Utilities/Tuples/FTupleStringNode.m34
-rw-r--r--Firebase/Database/Utilities/Tuples/FTupleTSN.h25
-rw-r--r--Firebase/Database/Utilities/Tuples/FTupleTSN.m24
-rw-r--r--Firebase/Database/Utilities/Tuples/FTupleTransaction.h74
-rw-r--r--Firebase/Database/Utilities/Tuples/FTupleTransaction.m38
-rw-r--r--Firebase/Database/Utilities/Tuples/FTupleUserCallback.h31
-rw-r--r--Firebase/Database/Utilities/Tuples/FTupleUserCallback.m35
-rw-r--r--Firebase/Database/module.modulemap13
-rw-r--r--Firebase/Database/third_party/SocketRocket/FSRWebSocket.h107
-rw-r--r--Firebase/Database/third_party/SocketRocket/FSRWebSocket.m1848
-rw-r--r--Firebase/Database/third_party/SocketRocket/NSData+SRB64Additions.h23
-rw-r--r--Firebase/Database/third_party/SocketRocket/NSData+SRB64Additions.m37
-rw-r--r--Firebase/Database/third_party/SocketRocket/aa2297808c225710e267afece4439c256f6efdb33
-rw-r--r--Firebase/Database/third_party/SocketRocket/fbase64.c318
-rw-r--r--Firebase/Database/third_party/SocketRocket/fbase64.h33
-rw-r--r--Firebase/Database/third_party/Wrap-leveldb/APLevelDB.h105
-rw-r--r--Firebase/Database/third_party/Wrap-leveldb/APLevelDB.mm500
-rw-r--r--Firebase/Firebase/Firebase.h73
-rwxr-xr-xFirebase/Firebase/module.modulemap4
-rw-r--r--Firebase/Messaging/FIRMMessageCode.h169
-rw-r--r--Firebase/Messaging/FIRMessaging+FIRApp.h24
-rw-r--r--Firebase/Messaging/FIRMessaging+FIRApp.m111
-rw-r--r--Firebase/Messaging/FIRMessaging.m1071
-rw-r--r--Firebase/Messaging/FIRMessagingCheckinService.h53
-rw-r--r--Firebase/Messaging/FIRMessagingCheckinService.m132
-rw-r--r--Firebase/Messaging/FIRMessagingClient.h156
-rw-r--r--Firebase/Messaging/FIRMessagingClient.m490
-rw-r--r--Firebase/Messaging/FIRMessagingCodedInputStream.h28
-rw-r--r--Firebase/Messaging/FIRMessagingCodedInputStream.m142
-rw-r--r--Firebase/Messaging/FIRMessagingConfig.h46
-rw-r--r--Firebase/Messaging/FIRMessagingConfig.m38
-rw-r--r--Firebase/Messaging/FIRMessagingConnection.h107
-rw-r--r--Firebase/Messaging/FIRMessagingConnection.m711
-rw-r--r--Firebase/Messaging/FIRMessagingConstants.h58
-rw-r--r--Firebase/Messaging/FIRMessagingConstants.m51
-rw-r--r--Firebase/Messaging/FIRMessagingContextManagerService.h44
-rw-r--r--Firebase/Messaging/FIRMessagingContextManagerService.m189
-rw-r--r--Firebase/Messaging/FIRMessagingDataMessageManager.h101
-rw-r--r--Firebase/Messaging/FIRMessagingDataMessageManager.m545
-rw-r--r--Firebase/Messaging/FIRMessagingDefines.h96
-rw-r--r--Firebase/Messaging/FIRMessagingDelayedMessageQueue.h35
-rw-r--r--Firebase/Messaging/FIRMessagingDelayedMessageQueue.m146
-rw-r--r--Firebase/Messaging/FIRMessagingFileLogger.h31
-rw-r--r--Firebase/Messaging/FIRMessagingFileLogger.m108
-rw-r--r--Firebase/Messaging/FIRMessagingInstanceIDProxy.h56
-rw-r--r--Firebase/Messaging/FIRMessagingInstanceIDProxy.m123
-rw-r--r--Firebase/Messaging/FIRMessagingLogger.h97
-rw-r--r--Firebase/Messaging/FIRMessagingLogger.m305
-rw-r--r--Firebase/Messaging/FIRMessagingPacketQueue.h43
-rw-r--r--Firebase/Messaging/FIRMessagingPacketQueue.m103
-rw-r--r--Firebase/Messaging/FIRMessagingPendingTopicsList.h118
-rw-r--r--Firebase/Messaging/FIRMessagingPendingTopicsList.m261
-rw-r--r--Firebase/Messaging/FIRMessagingPersistentSyncMessage.h28
-rw-r--r--Firebase/Messaging/FIRMessagingPersistentSyncMessage.m54
-rw-r--r--Firebase/Messaging/FIRMessagingPubSub.h148
-rw-r--r--Firebase/Messaging/FIRMessagingPubSub.m278
-rw-r--r--Firebase/Messaging/FIRMessagingPubSubRegistrar.h56
-rw-r--r--Firebase/Messaging/FIRMessagingPubSubRegistrar.m78
-rw-r--r--Firebase/Messaging/FIRMessagingReceiver.h31
-rw-r--r--Firebase/Messaging/FIRMessagingReceiver.m141
-rw-r--r--Firebase/Messaging/FIRMessagingRegistrar.h87
-rw-r--r--Firebase/Messaging/FIRMessagingRegistrar.m112
-rw-r--r--Firebase/Messaging/FIRMessagingRemoteNotificationsProxy.h40
-rw-r--r--Firebase/Messaging/FIRMessagingRemoteNotificationsProxy.m613
-rw-r--r--Firebase/Messaging/FIRMessagingRmq2PersistentStore.h201
-rw-r--r--Firebase/Messaging/FIRMessagingRmq2PersistentStore.m770
-rw-r--r--Firebase/Messaging/FIRMessagingRmqManager.h190
-rw-r--r--Firebase/Messaging/FIRMessagingRmqManager.m264
-rw-r--r--Firebase/Messaging/FIRMessagingSecureSocket.h56
-rw-r--r--Firebase/Messaging/FIRMessagingSecureSocket.m448
-rw-r--r--Firebase/Messaging/FIRMessagingSyncMessageManager.h59
-rw-r--r--Firebase/Messaging/FIRMessagingSyncMessageManager.m147
-rw-r--r--Firebase/Messaging/FIRMessagingTopicOperation.h45
-rw-r--r--Firebase/Messaging/FIRMessagingTopicOperation.m246
-rw-r--r--Firebase/Messaging/FIRMessagingTopicsCommon.h52
-rw-r--r--Firebase/Messaging/FIRMessagingUtilities.h54
-rw-r--r--Firebase/Messaging/FIRMessagingUtilities.m173
-rw-r--r--Firebase/Messaging/FIRMessagingVersionUtilities.h35
-rw-r--r--Firebase/Messaging/FIRMessagingVersionUtilities.m87
-rw-r--r--Firebase/Messaging/FIRMessaging_Private.h56
-rw-r--r--Firebase/Messaging/FirebaseMessaging.h17
-rw-r--r--Firebase/Messaging/FirebaseMessaging.podspec41
-rw-r--r--Firebase/Messaging/InternalHeaders/FIRMessagingInternalUtilities.h30
-rw-r--r--Firebase/Messaging/NSDictionary+FIRMessaging.h45
-rw-r--r--Firebase/Messaging/NSDictionary+FIRMessaging.m59
-rw-r--r--Firebase/Messaging/NSError+FIRMessaging.h68
-rw-r--r--Firebase/Messaging/NSError+FIRMessaging.m35
-rw-r--r--Firebase/Messaging/Protos/GtalkCore.pbobjc.h1344
-rw-r--r--Firebase/Messaging/Protos/GtalkCore.pbobjc.m2947
-rw-r--r--Firebase/Messaging/Protos/GtalkExtensions.pbobjc.h617
-rw-r--r--Firebase/Messaging/Protos/GtalkExtensions.pbobjc.m1407
-rw-r--r--Firebase/Messaging/Public/FIRMessaging.h486
-rwxr-xr-xFirebase/Messaging/Public/FirebaseMessaging.h17
-rw-r--r--Firebase/Storage/FIRStorage.h130
-rw-r--r--Firebase/Storage/FIRStorage.m233
-rw-r--r--Firebase/Storage/FIRStorageConstants.h173
-rw-r--r--Firebase/Storage/FIRStorageConstants.m83
-rw-r--r--Firebase/Storage/FIRStorageDeleteTask.m54
-rw-r--r--Firebase/Storage/FIRStorageDownloadTask.h39
-rw-r--r--Firebase/Storage/FIRStorageDownloadTask.m162
-rw-r--r--Firebase/Storage/FIRStorageErrors.m172
-rw-r--r--Firebase/Storage/FIRStorageGetMetadataTask.m84
-rw-r--r--Firebase/Storage/FIRStorageMetadata.h149
-rw-r--r--Firebase/Storage/FIRStorageMetadata.m227
-rw-r--r--Firebase/Storage/FIRStorageObservableTask.h63
-rw-r--r--Firebase/Storage/FIRStorageObservableTask.m216
-rw-r--r--Firebase/Storage/FIRStoragePath.m199
-rw-r--r--Firebase/Storage/FIRStorageReference.h244
-rw-r--r--Firebase/Storage/FIRStorageReference.m364
-rw-r--r--Firebase/Storage/FIRStorageSwiftNameSupport.h29
-rw-r--r--Firebase/Storage/FIRStorageTask.h76
-rw-r--r--Firebase/Storage/FIRStorageTask.m65
-rw-r--r--Firebase/Storage/FIRStorageTaskSnapshot.h68
-rw-r--r--Firebase/Storage/FIRStorageTaskSnapshot.m88
-rw-r--r--Firebase/Storage/FIRStorageTokenAuthorizer.m131
-rw-r--r--Firebase/Storage/FIRStorageUpdateMetadataTask.m91
-rw-r--r--Firebase/Storage/FIRStorageUploadTask.h39
-rw-r--r--Firebase/Storage/FIRStorageUploadTask.m199
-rw-r--r--Firebase/Storage/FIRStorageUtils.m121
-rw-r--r--Firebase/Storage/FirebaseStorage.h25
-rw-r--r--Firebase/Storage/FirebaseStorage.podspec44
-rw-r--r--Firebase/Storage/Private/FIRStorageConstants_Private.h145
-rw-r--r--Firebase/Storage/Private/FIRStorageDeleteTask.h34
-rw-r--r--Firebase/Storage/Private/FIRStorageDownloadTask_Private.h52
-rw-r--r--Firebase/Storage/Private/FIRStorageErrors.h54
-rw-r--r--Firebase/Storage/Private/FIRStorageGetMetadataTask.h34
-rw-r--r--Firebase/Storage/Private/FIRStorageMetadata_Private.h52
-rw-r--r--Firebase/Storage/Private/FIRStorageObservableTask_Private.h45
-rw-r--r--Firebase/Storage/Private/FIRStoragePath.h106
-rw-r--r--Firebase/Storage/Private/FIRStorageReference_Private.h37
-rw-r--r--Firebase/Storage/Private/FIRStorageTaskSnapshot_Private.h56
-rw-r--r--Firebase/Storage/Private/FIRStorageTask_Private.h77
-rw-r--r--Firebase/Storage/Private/FIRStorageTokenAuthorizer.h44
-rw-r--r--Firebase/Storage/Private/FIRStorageUpdateMetadataTask.h35
-rw-r--r--Firebase/Storage/Private/FIRStorageUploadTask_Private.h69
-rw-r--r--Firebase/Storage/Private/FIRStorageUtils.h93
-rw-r--r--Firebase/Storage/Private/FIRStorage_Private.h38
570 files changed, 71542 insertions, 0 deletions
diff --git a/Firebase/Auth/CHANGELOG.md b/Firebase/Auth/CHANGELOG.md
new file mode 100644
index 0000000..d2eb535
--- /dev/null
+++ b/Firebase/Auth/CHANGELOG.md
@@ -0,0 +1,62 @@
+# 2017-05-17 -- v4.0.0
+- Adds Phone Number Authentication.
+- Adds support for generic OAuth2 identity providers.
+- Adds methods that return additional user data from identity providers if
+ available when authenticating users.
+- Improves session management by automatically refreshing tokens if possible
+ and signing out users if the session is detected invalidated, for example,
+ after the user changed password or deleted account from another device.
+- Fixes an issue that reauthentication creates new user account if the user
+ credential is valid but does not match the currently signed in user.
+- Fixes an issue that the "password" provider is not immediately listed on the
+ client side after adding a password to an account.
+- Changes factory methods to return non-null FIRAuth instances or raises an
+ exception, instead of returning nullable instances.
+- Changes auth state change listener to only be triggered when the user changes.
+- Adds a new listener which is triggered whenever the ID token is changed.
+- Switches ERROR_EMAIL_ALREADY_IN_USE to
+ ERROR_ACCOUNT_EXISTS_WITH_DIFFERENT_CREDENTIAL when the email used in the
+ signInWithCredential: call is already in use by another account.
+- Deprecates FIREmailPasswordAuthProvider in favor of FIREmailAuthProvider.
+- Deprecates getTokenWithCompletion in favor of getIDTokenWithCompletion on
+ FIRUser.
+- Changes Swift API names to better align with Swift convention.
+
+# 2017-02-06 -- v3.1.1
+- Allows handling of additional errors when sending OOB action emails. The
+ server can respond with the following new error messages:
+ INVALID_MESSAGE_PAYLOAD,INVALID_SENDER and INVALID_RECIPIENT_EMAIL.
+- Removes incorrect reference to FIRAuthErrorCodeCredentialTooOld in FIRUser.h.
+- Provides additional error information from server if available.
+
+# 2016-12-13 -- v3.1.0
+- Adds FIRAuth methods that enable the app to follow up with user actions
+ delivered by email, such as verifying email address or reset password.
+- No longer applies the keychain workaround introduced in v3.0.5 on iOS 10.2
+ simulator or above since the issue has been fixed.
+- Fixes nullability compilation warnings when used in Swift.
+- Better reports missing password error.
+
+# 2016-10-24 -- v3.0.6
+- Switches to depend on open sourced GoogleToolboxForMac and GTMSessionFetcher.
+- Improves logging of keychain error when initializing.
+
+# 2016-09-14 -- v3.0.5
+- Works around a keychain issue in iOS 10 simulator.
+- Reports the correct error for invalid email when signing in with email and
+ password.
+
+# 2016-07-18 -- v3.0.4
+- Fixes a race condition bug that could crash the app with an exception from
+ NSURLSession on iOS 9.
+
+# 2016-06-20 -- v3.0.3
+- Adds documentation for all possible errors returned by each method.
+- Improves error handling and messages for a variety of error conditions.
+- Whether or not an user is considered anonymous is now consistent with other
+ platforms.
+- A saved signed in user is now siloed between different Firebase projects
+ within the same app.
+
+# 2016-05-18 -- v3.0.2
+- Initial public release.
diff --git a/Firebase/Auth/Docs/threading.md b/Firebase/Auth/Docs/threading.md
new file mode 100644
index 0000000..2f5b782
--- /dev/null
+++ b/Firebase/Auth/Docs/threading.md
@@ -0,0 +1,119 @@
+# Firebase Auth Thread Safety
+
+This document describes how Firebase Auth maintains thread-safety. The Firebase
+Auth library (not including Firebase Auth UI and Auth Provider UIs for now)
+must be thread-safe, meaning deveopers are free to call any method in any
+thread at any time. Thus, all code that may take part in race conditions must
+be protected in some way.
+
+## Local Synchronization
+
+When contested data and accessing code is limited in scope, for example,
+a mutable array accessed only by two methods, a `@synchronized` directive is
+probably the simplest solution. Make sure the object to be locked on is not
+`nil`, e.g., `self`.
+
+## Global Work Queue
+
+A more scalable solution used throughout the current code base is to execute
+all potentially conflicting code in the same serial dispatch queue, which is
+referred as "the auth global work queue", or in some other serial queue that
+has its target queue set to this auth global work queue. This way we don't
+have to think about which variables may be contested. We only need to make
+sure all public APIs that may have thread-safety issues make the dispatch.
+The auth global work queue is defined in
+[FIRAuthGlobalWorkQueue.h](../Source/Private/FIRAuthGlobalWorkQueue.h)
+and any serial task queue created by
+[FIRAuthSerialTaskQueue.h](../Source/Private/FIRAuthSerialTaskQueue.h)
+already has its target set properly.
+
+In following sub-sections, we divided methods into three categories, according
+to the two criteria below:
+
+1. Whether the method is public or private:
+ * A public method can be directly called by developers.
+ * A private method can only be called by our own code.
+2. Whether the method is synchronous or asynchronous.
+ * A synchronous method returns some value or object in the calling
+ thread immediately.
+ * An asynchronous method returns nothing but calls the callback provided
+ by the caller at some point in future.
+
+### Public Asynchronous Methods
+
+Unless it's a simple wrapper of another public asynchronous method, a public
+asynchronous method shall
+
+* Dispatch asynchronously to the auth global work queue immediately.
+* Dispatch asynchronously to the main queue before calling the callback.
+ This is to make developers' life easier so they don't have to manage
+ thread-safety.
+
+The code would look like:
+
+```objectivec
+- (void)doSomethingWithCompletion:(nullable CompletionBlock)completion {
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^{
+ // Do things...
+ if (completion) {
+ dispatch_async(dispatch_get_main_queue(), ^{
+ completion(args);
+ });
+ }
+ });
+}
+```
+
+### Public Synchronous Methods
+
+A public synchronous method that needs protection shall dispatch
+*synchronously* to the auth global work queue for its work. The code would
+look like:
+
+```objectivec
+- (ReturnType)something {
+ __block ReturnType result;
+ dispatch_sync(FIRAuthGlobalWorkQueue(), ^{
+ // Compute result.
+ result = computedResult;
+ });
+ return result;
+}
+```
+
+**But don't call methods protected this way from private methods, or a
+deadlock would occur.** This is because you are not supposed to
+`dispatch_sync` to the queue you're already in. This can be easily worked
+around by creating an equivalent private synchronous method to be called by
+both public and private methods and making the public synchronous method a
+wrapper of that. For example,
+
+```objectivec
+- (ReturnType)somethingInternal {
+ // Compute result.
+ return computedResult;
+}
+
+- (ReturnType)something {
+ __block ReturnType result;
+ dispatch_sync(FIRAuthGlobalWorkQueue(), ^{
+ result = [self somethingInternal];
+ });
+ return result;
+}
+```
+
+### Private Methods
+
+Generally speaking there is nothing special needed to be done for private
+methods:
+
+* The calling code should already be in the auth global work queue.
+* The callback, if any, is provided by our own code, so it expects to called
+ in the auth global work queue as well. This is usually already the case,
+ unless the method pass the callback to some other asychronous methods
+ outside our library, in which case we need to manually make the callback
+ called in the auth global work queue.
+
+Just beware you can't call public synchronous methods protected by the auth
+global work queue from private methods as stated in the preceding sub-section.
diff --git a/Firebase/Auth/FirebaseAuth.podspec b/Firebase/Auth/FirebaseAuth.podspec
new file mode 100644
index 0000000..74aa07c
--- /dev/null
+++ b/Firebase/Auth/FirebaseAuth.podspec
@@ -0,0 +1,56 @@
+# This podspec is not intended to be deployed. It is solely for the static
+# library framework build process at
+# https://github.com/firebase/firebase-ios-sdk/tree/master/BuildFrameworks
+
+Pod::Spec.new do |s|
+ s.name = 'FirebaseAuth'
+ s.version = '4.0.0'
+ s.summary = 'Firebase Open Source Libraries for iOS.'
+
+ s.description = <<-DESC
+Simplify your iOS development, grow your user base, and monetize more effectively with Firebase.
+ DESC
+
+ s.homepage = 'https://firebase.google.com'
+ s.license = { :type => 'Apache', :file => '../../LICENSE' }
+ s.authors = 'Google, Inc.'
+
+ # NOTE that the FirebaseDev pod is neither publicly deployed nor yet interchangeable with the
+ # Firebase pod
+ s.source = { :git => 'https://github.com/firebase/firebase-ios-sdk.git', :tag => s.version.to_s }
+ s.social_media_url = 'https://twitter.com/Firebase'
+ s.ios.deployment_target = '7.0'
+
+ s.source_files = '**/*.[mh]'
+ s.public_header_files =
+ 'Source/FirebaseAuth.h',
+ 'Source/FirebaseAuthVersion.h',
+ 'Source/FIRAdditionalUserInfo.h',
+ 'Source/FIRAuth.h',
+ 'Source/FIRAuthAPNSTokenType.h',
+ 'Source/FIRAuthCredential.h',
+ 'Source/FIRAuthDataResult.h',
+ 'Source/FIRAuthErrors.h',
+ 'Source/FIRAuthSwiftNameSupport.h',
+ 'Source/AuthProviders/EmailPassword/FIREmailAuthProvider.h',
+ 'Source/AuthProviders/Facebook/FIRFacebookAuthProvider.h',
+ 'Source/AuthProviders/GitHub/FIRGitHubAuthProvider.h',
+ 'Source/AuthProviders/Google/FIRGoogleAuthProvider.h',
+ 'Source/AuthProviders/OAuth/FIROAuthProvider.h',
+ 'Source/AuthProviders/Phone/FIRPhoneAuthCredential.h',
+ 'Source/AuthProviders/Phone/FIRPhoneAuthProvider.h',
+ 'Source/AuthProviders/Twitter/FIRTwitterAuthProvider.h',
+ 'Source/FIRUser.h',
+ 'Source/FIRUserInfo.h'
+ s.preserve_paths =
+ 'README.md',
+ 'CHANGELOG.md'
+ s.xcconfig = { 'GCC_PREPROCESSOR_DEFINITIONS' =>
+ '$(inherited) ' + 'FIRAuth_VERSION=' + s.version.to_s +
+ ' FIRAuth_MINOR_VERSION=' + s.version.to_s.split(".")[0] + "." + s.version.to_s.split(".")[1]
+ }
+ s.framework = 'Security'
+# s.dependency 'FirebaseDev/Core'
+ s.dependency 'GTMSessionFetcher/Core', '~> 1.1'
+ s.dependency 'GoogleToolboxForMac/NSDictionary+URLArguments', '~> 2.1'
+end
diff --git a/Firebase/Auth/README.md b/Firebase/Auth/README.md
new file mode 100644
index 0000000..e766949
--- /dev/null
+++ b/Firebase/Auth/README.md
@@ -0,0 +1,8 @@
+# Firebase Auth for iOS
+
+Firebase Auth enables apps to easily support multiple authentication options
+for their end users.
+
+Please visit [our developer site](https://developers.google.com/) for
+integration instructions, documentation, support information, and terms of
+service.
diff --git a/Firebase/Auth/Source/AuthProviders/EmailPassword/FIREmailAuthProvider.h b/Firebase/Auth/Source/AuthProviders/EmailPassword/FIREmailAuthProvider.h
new file mode 100644
index 0000000..4fb5ea0
--- /dev/null
+++ b/Firebase/Auth/Source/AuthProviders/EmailPassword/FIREmailAuthProvider.h
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+#import "FIRAuthSwiftNameSupport.h"
+
+@class FIRAuthCredential;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ @brief A string constant identifying the email & password identity provider.
+ */
+extern NSString *const FIREmailAuthProviderID FIR_SWIFT_NAME(EmailAuthProviderID);
+
+/**
+ @brief please use @c FIREmailAuthProviderID instead.
+ */
+extern NSString *const FIREmailPasswordAuthProviderID __attribute__((deprecated));
+
+/** @class FIREmailAuthProvider
+ @brief A concrete implementation of @c FIRAuthProvider for Email & Password Sign In.
+ */
+FIR_SWIFT_NAME(EmailAuthProvider)
+@interface FIREmailAuthProvider : NSObject
+
+/** @typedef FIREmailPasswordAuthProvider
+ @brief Please use @c FIREmailAuthProvider instead.
+ */
+typedef FIREmailAuthProvider FIREmailPasswordAuthProvider __attribute__((deprecated));
+
+
+/** @fn credentialWithEmail:password:
+ @brief Creates an @c FIRAuthCredential for an email & password sign in.
+
+ @param email The user's email address.
+ @param password The user's password.
+ @return A FIRAuthCredential containing the email & password credential.
+ */
++ (FIRAuthCredential *)credentialWithEmail:(NSString *)email password:(NSString *)password;
+
+/** @fn init
+ @brief This class is not meant to be initialized.
+ */
+- (instancetype)init NS_UNAVAILABLE;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/AuthProviders/EmailPassword/FIREmailAuthProvider.m b/Firebase/Auth/Source/AuthProviders/EmailPassword/FIREmailAuthProvider.m
new file mode 100644
index 0000000..d27611e
--- /dev/null
+++ b/Firebase/Auth/Source/AuthProviders/EmailPassword/FIREmailAuthProvider.m
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIREmailAuthProvider.h"
+
+#import "FIREmailPasswordAuthCredential.h"
+
+// FIREmailPasswordAuthProviderID is defined in FIRAuthProvider.m.
+
+@implementation FIREmailAuthProvider
+
+- (instancetype)init {
+ @throw [NSException exceptionWithName:@"Attempt to call unavailable initializer."
+ reason:@"This class is not meant to be initialized."
+ userInfo:nil];
+}
+
++ (FIRAuthCredential *)credentialWithEmail:(NSString *)email password:(NSString *)password {
+ return [[FIREmailPasswordAuthCredential alloc] initWithEmail:email password:password];
+}
+
+@end
diff --git a/Firebase/Auth/Source/AuthProviders/EmailPassword/FIREmailPasswordAuthCredential.h b/Firebase/Auth/Source/AuthProviders/EmailPassword/FIREmailPasswordAuthCredential.h
new file mode 100644
index 0000000..004716c
--- /dev/null
+++ b/Firebase/Auth/Source/AuthProviders/EmailPassword/FIREmailPasswordAuthCredential.h
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+#import "../../Private/FIRAuthCredential_Internal.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @class FIREmailPasswordAuthCredential
+ @brief Internal implementation of FIRAuthCredential for Email/Password credentials.
+ */
+@interface FIREmailPasswordAuthCredential : FIRAuthCredential
+
+/** @property email
+ @brief The user's email address.
+ */
+@property(nonatomic, readonly) NSString *email;
+
+/** @property password
+ @brief The user's password.
+ */
+@property(nonatomic, readonly) NSString *password;
+
+/** @fn initWithEmail:password:
+ @brief Designated initializer.
+ @param email The user's email address.
+ @param password The user's password.
+ */
+- (nullable instancetype)initWithEmail:(NSString *)email password:(NSString *)password
+ NS_DESIGNATED_INITIALIZER;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/AuthProviders/EmailPassword/FIREmailPasswordAuthCredential.m b/Firebase/Auth/Source/AuthProviders/EmailPassword/FIREmailPasswordAuthCredential.m
new file mode 100644
index 0000000..4361366
--- /dev/null
+++ b/Firebase/Auth/Source/AuthProviders/EmailPassword/FIREmailPasswordAuthCredential.m
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIREmailPasswordAuthCredential.h"
+
+#import "FIREmailAuthProvider.h"
+#import "FIRAuthExceptionUtils.h"
+#import "FIRVerifyAssertionRequest.h"
+
+@interface FIREmailPasswordAuthCredential ()
+
+- (nullable instancetype)initWithProvider:(NSString *)provider NS_UNAVAILABLE;
+
+@end
+
+@implementation FIREmailPasswordAuthCredential
+
+- (nullable instancetype)initWithProvider:(NSString *)provider {
+ [FIRAuthExceptionUtils raiseMethodNotImplementedExceptionWithReason:
+ @"Please call the designated initializer."];
+ return nil;
+}
+
+- (nullable instancetype)initWithEmail:(NSString *)email password:(NSString *)password {
+ self = [super initWithProvider:FIREmailAuthProviderID];
+ if (self) {
+ _email = [email copy];
+ _password = [password copy];
+ }
+ return self;
+}
+
+- (void)prepareVerifyAssertionRequest:(FIRVerifyAssertionRequest *)request {
+ [FIRAuthExceptionUtils raiseMethodNotImplementedExceptionWithReason:
+ @"Attempt to call prepareVerifyAssertionRequest: on a FIREmailPasswordAuthCredential."];
+}
+
+@end
diff --git a/Firebase/Auth/Source/AuthProviders/EmailPassword/FIREmailPasswordAuthProvider.h b/Firebase/Auth/Source/AuthProviders/EmailPassword/FIREmailPasswordAuthProvider.h
new file mode 100644
index 0000000..bf7db21
--- /dev/null
+++ b/Firebase/Auth/Source/AuthProviders/EmailPassword/FIREmailPasswordAuthProvider.h
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+@class FIRAuthCredential;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ @brief A string constant identifying the email & password identity provider.
+ */
+extern NSString *const FIREmailPasswordAuthProviderID;
+
+/** @class FIREmailPasswordAuthProvider
+ @brief A concrete implementation of @c FIRAuthProvider for Email & Password Sign In.
+ */
+@interface FIREmailPasswordAuthProvider : NSObject
+
+/** @fn credentialWithEmail:password:
+ @brief Creates an @c FIRAuthCredential for an email & password sign in.
+
+ @param email The user's email address.
+ @param password The user's password.
+ @return A FIRAuthCredential containing the email & password credential.
+ */
++ (FIRAuthCredential *)credentialWithEmail:(NSString *)email password:(NSString *)password;
+
+/** @fn init
+ @brief This class is not meant to be initialized.
+ */
+- (instancetype)init NS_UNAVAILABLE;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/AuthProviders/EmailPassword/FIREmailPasswordAuthProvider.m b/Firebase/Auth/Source/AuthProviders/EmailPassword/FIREmailPasswordAuthProvider.m
new file mode 100644
index 0000000..84c3787
--- /dev/null
+++ b/Firebase/Auth/Source/AuthProviders/EmailPassword/FIREmailPasswordAuthProvider.m
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIREmailPasswordAuthProvider.h"
+
+#import "FIREmailPasswordAuthCredential.h"
+
+// FIREmailPasswordAuthProviderID is defined in FIRAuthProvider.m.
+
+@implementation FIREmailPasswordAuthProvider
+
+- (instancetype)init {
+ @throw [NSException exceptionWithName:@"Attempt to call unavailable initializer."
+ reason:@"This class is not meant to be initialized."
+ userInfo:nil];
+}
+
++ (FIRAuthCredential *)credentialWithEmail:(NSString *)email password:(NSString *)password {
+ return [[FIREmailPasswordAuthCredential alloc] initWithEmail:email password:password];
+}
+
+@end
diff --git a/Firebase/Auth/Source/AuthProviders/Facebook/FIRFacebookAuthCredential.h b/Firebase/Auth/Source/AuthProviders/Facebook/FIRFacebookAuthCredential.h
new file mode 100644
index 0000000..1c03573
--- /dev/null
+++ b/Firebase/Auth/Source/AuthProviders/Facebook/FIRFacebookAuthCredential.h
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+#import "../../Private/FIRAuthCredential_Internal.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @class FIRFacebookAuthCredential
+ @brief Internal implementation of FIRAuthCredential for the Facebook IdP.
+ */
+@interface FIRFacebookAuthCredential : FIRAuthCredential
+
+/** @fn initWithAccessToken:
+ @brief Designated initializer.
+ @param accessToken The Access Token obtained from Facebook.
+ */
+- (nullable instancetype)initWithAccessToken:(NSString *)accessToken NS_DESIGNATED_INITIALIZER;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/AuthProviders/Facebook/FIRFacebookAuthCredential.m b/Firebase/Auth/Source/AuthProviders/Facebook/FIRFacebookAuthCredential.m
new file mode 100644
index 0000000..1c3576a
--- /dev/null
+++ b/Firebase/Auth/Source/AuthProviders/Facebook/FIRFacebookAuthCredential.m
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRFacebookAuthCredential.h"
+
+#import "FIRFacebookAuthProvider.h"
+#import "FIRAuthExceptionUtils.h"
+#import "FIRVerifyAssertionRequest.h"
+
+@interface FIRFacebookAuthCredential ()
+
+- (nullable instancetype)initWithProvider:(NSString *)provider NS_UNAVAILABLE;
+
+@end
+
+@implementation FIRFacebookAuthCredential {
+ NSString *_accessToken;
+}
+
+- (nullable instancetype)initWithProvider:(NSString *)provider {
+ [FIRAuthExceptionUtils raiseMethodNotImplementedExceptionWithReason:
+ @"Please call the designated initializer."];
+ return nil;
+}
+
+- (nullable instancetype)initWithAccessToken:(NSString *)accessToken {
+ self = [super initWithProvider:FIRFacebookAuthProviderID];
+ if (self) {
+ _accessToken = [accessToken copy];
+ }
+ return self;
+}
+
+- (void)prepareVerifyAssertionRequest:(FIRVerifyAssertionRequest *)request {
+ request.providerAccessToken = _accessToken;
+}
+
+@end
diff --git a/Firebase/Auth/Source/AuthProviders/Facebook/FIRFacebookAuthProvider.h b/Firebase/Auth/Source/AuthProviders/Facebook/FIRFacebookAuthProvider.h
new file mode 100644
index 0000000..2307b08
--- /dev/null
+++ b/Firebase/Auth/Source/AuthProviders/Facebook/FIRFacebookAuthProvider.h
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+#import "FIRAuthSwiftNameSupport.h"
+
+@class FIRAuthCredential;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ @brief A string constant identifying the Facebook identity provider.
+ */
+extern NSString *const FIRFacebookAuthProviderID FIR_SWIFT_NAME(FacebookAuthProviderID);
+
+/** @class FIRFacebookAuthProvider
+ @brief Utility class for constructing Facebook credentials.
+ */
+FIR_SWIFT_NAME(FacebookAuthProvider)
+@interface FIRFacebookAuthProvider : NSObject
+
+/** @fn credentialWithAccessToken:
+ @brief Creates an @c FIRAuthCredential for a Facebook sign in.
+
+ @param accessToken The Access Token from Facebook.
+ @return A FIRAuthCredential containing the Facebook credentials.
+ */
++ (FIRAuthCredential *)credentialWithAccessToken:(NSString *)accessToken;
+
+/** @fn init
+ @brief This class should not be initialized.
+ */
+- (instancetype)init NS_UNAVAILABLE;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/AuthProviders/Facebook/FIRFacebookAuthProvider.m b/Firebase/Auth/Source/AuthProviders/Facebook/FIRFacebookAuthProvider.m
new file mode 100644
index 0000000..d2759ae
--- /dev/null
+++ b/Firebase/Auth/Source/AuthProviders/Facebook/FIRFacebookAuthProvider.m
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRFacebookAuthProvider.h"
+
+#import "FIRFacebookAuthCredential.h"
+#import "FIRAuthExceptionUtils.h"
+
+// FIRFacebookAuthProviderID is defined in FIRAuthProvider.m.
+
+@implementation FIRFacebookAuthProvider
+
+- (instancetype)init {
+ [FIRAuthExceptionUtils raiseMethodNotImplementedExceptionWithReason:
+ @"This class is not meant to be initialized."];
+ return nil;
+}
+
++ (FIRAuthCredential *)credentialWithAccessToken:(NSString *)accessToken {
+ return [[FIRFacebookAuthCredential alloc] initWithAccessToken:accessToken];
+}
+
+@end
diff --git a/Firebase/Auth/Source/AuthProviders/GitHub/FIRGitHubAuthCredential.h b/Firebase/Auth/Source/AuthProviders/GitHub/FIRGitHubAuthCredential.h
new file mode 100644
index 0000000..c43fb52
--- /dev/null
+++ b/Firebase/Auth/Source/AuthProviders/GitHub/FIRGitHubAuthCredential.h
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+#import "../../Private/FIRAuthCredential_Internal.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @class FIRGitHubAuthCredential
+ @brief Internal implementation of FIRAuthCredential for GitHub credentials.
+ */
+@interface FIRGitHubAuthCredential : FIRAuthCredential
+
+/** @property token
+ @brief The GitHub OAuth access token.
+ */
+@property(nonatomic, readonly) NSString *token;
+
+/** @fn initWithToken:
+ @brief Designated initializer.
+ @param token The GitHub OAuth access token.
+ */
+- (nullable instancetype)initWithToken:(NSString *)token NS_DESIGNATED_INITIALIZER;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/AuthProviders/GitHub/FIRGitHubAuthCredential.m b/Firebase/Auth/Source/AuthProviders/GitHub/FIRGitHubAuthCredential.m
new file mode 100644
index 0000000..a0185eb
--- /dev/null
+++ b/Firebase/Auth/Source/AuthProviders/GitHub/FIRGitHubAuthCredential.m
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRGitHubAuthCredential.h"
+
+#import "FIRGitHubAuthProvider.h"
+#import "FIRAuthExceptionUtils.h"
+#import "FIRVerifyAssertionRequest.h"
+
+@interface FIRGitHubAuthCredential ()
+
+- (nullable instancetype)initWithProvider:(NSString *)provider NS_UNAVAILABLE;
+
+@end
+
+@implementation FIRGitHubAuthCredential
+
+- (nullable instancetype)initWithProvider:(NSString *)provider {
+ [FIRAuthExceptionUtils raiseMethodNotImplementedExceptionWithReason:
+ @"Please call the designated initializer."];
+ return nil;
+}
+
+- (nullable instancetype)initWithToken:(NSString *)token {
+ self = [super initWithProvider:FIRGitHubAuthProviderID];
+ if (self) {
+ _token = [token copy];
+ }
+ return self;
+}
+
+- (void)prepareVerifyAssertionRequest:(FIRVerifyAssertionRequest *)request {
+ request.providerAccessToken = _token;
+}
+
+@end
diff --git a/Firebase/Auth/Source/AuthProviders/GitHub/FIRGitHubAuthProvider.h b/Firebase/Auth/Source/AuthProviders/GitHub/FIRGitHubAuthProvider.h
new file mode 100644
index 0000000..ab5c0ef
--- /dev/null
+++ b/Firebase/Auth/Source/AuthProviders/GitHub/FIRGitHubAuthProvider.h
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+#import "FIRAuthSwiftNameSupport.h"
+
+@class FIRAuthCredential;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ @brief A string constant identifying the GitHub identity provider.
+ */
+extern NSString *const FIRGitHubAuthProviderID FIR_SWIFT_NAME(GitHubAuthProviderID);
+
+/** @class FIRGitHubAuthProvider
+ @brief Utility class for constructing GitHub credentials.
+ */
+FIR_SWIFT_NAME(GitHubAuthProvider)
+@interface FIRGitHubAuthProvider : NSObject
+
+/** @fn credentialWithToken:
+ @brief Creates an @c FIRAuthCredential for a GitHub sign in.
+
+ @param token The GitHub OAuth access token.
+ @return A FIRAuthCredential containing the GitHub credential.
+ */
++ (FIRAuthCredential *)credentialWithToken:(NSString *)token;
+
+/** @fn init
+ @brief This class is not meant to be initialized.
+ */
+- (instancetype)init NS_UNAVAILABLE;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/AuthProviders/GitHub/FIRGitHubAuthProvider.m b/Firebase/Auth/Source/AuthProviders/GitHub/FIRGitHubAuthProvider.m
new file mode 100644
index 0000000..8e0ff76
--- /dev/null
+++ b/Firebase/Auth/Source/AuthProviders/GitHub/FIRGitHubAuthProvider.m
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRGitHubAuthProvider.h"
+
+#import "FIRGitHubAuthCredential.h"
+#import "FIRAuthExceptionUtils.h"
+
+// FIRGitHubAuthProviderID is defined in FIRAuthProvider.m.
+
+@implementation FIRGitHubAuthProvider
+
+- (instancetype)init {
+ [FIRAuthExceptionUtils raiseMethodNotImplementedExceptionWithReason:
+ @"This class is not meant to be initialized."];
+ return nil;
+}
+
++ (FIRAuthCredential *)credentialWithToken:(NSString *)token {
+ return [[FIRGitHubAuthCredential alloc] initWithToken:token];
+}
+
+@end
diff --git a/Firebase/Auth/Source/AuthProviders/Google/FIRGoogleAuthCredential.h b/Firebase/Auth/Source/AuthProviders/Google/FIRGoogleAuthCredential.h
new file mode 100644
index 0000000..ae98fbc
--- /dev/null
+++ b/Firebase/Auth/Source/AuthProviders/Google/FIRGoogleAuthCredential.h
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+#import "../../Private/FIRAuthCredential_Internal.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @class FIRGoogleAuthCredential
+ @brief Internal implementation of FIRAuthCredential for the Google IdP.
+ */
+@interface FIRGoogleAuthCredential : FIRAuthCredential
+
+/** @fn initWithIDToken:accessToken:
+ @brief Designated initializer.
+ @param IDToken The ID Token obtained from Google.
+ @param accessToken The Access Token obtained from Google.
+ */
+- (nullable instancetype)initWithIDToken:(NSString *)IDToken accessToken:(NSString *)accessToken
+ NS_DESIGNATED_INITIALIZER;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/AuthProviders/Google/FIRGoogleAuthCredential.m b/Firebase/Auth/Source/AuthProviders/Google/FIRGoogleAuthCredential.m
new file mode 100644
index 0000000..d66b2e2
--- /dev/null
+++ b/Firebase/Auth/Source/AuthProviders/Google/FIRGoogleAuthCredential.m
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRGoogleAuthCredential.h"
+
+#import "FIRGoogleAuthProvider.h"
+#import "FIRAuthExceptionUtils.h"
+#import "FIRVerifyAssertionRequest.h"
+
+@interface FIRGoogleAuthCredential ()
+
+- (nullable instancetype)initWithProvider:(NSString *)provider NS_UNAVAILABLE;
+
+@end
+
+@implementation FIRGoogleAuthCredential {
+ NSString *_IDToken;
+ NSString *_accessToken;
+}
+
+- (nullable instancetype)initWithProvider:(NSString *)provider {
+ [FIRAuthExceptionUtils raiseMethodNotImplementedExceptionWithReason:
+ @"Please call the designated initializer."];
+ return nil;
+}
+
+- (nullable instancetype)initWithIDToken:(NSString *)IDToken accessToken:(NSString *)accessToken {
+ self = [super initWithProvider:FIRGoogleAuthProviderID];
+ if (self) {
+ _IDToken = [IDToken copy];
+ _accessToken = [accessToken copy];
+ }
+ return self;
+}
+
+- (void)prepareVerifyAssertionRequest:(FIRVerifyAssertionRequest *)request {
+ request.providerIDToken = _IDToken;
+ request.providerAccessToken = _accessToken;
+}
+
+@end
diff --git a/Firebase/Auth/Source/AuthProviders/Google/FIRGoogleAuthProvider.h b/Firebase/Auth/Source/AuthProviders/Google/FIRGoogleAuthProvider.h
new file mode 100644
index 0000000..92f0db2
--- /dev/null
+++ b/Firebase/Auth/Source/AuthProviders/Google/FIRGoogleAuthProvider.h
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+#import "FIRAuthSwiftNameSupport.h"
+
+@class FIRAuthCredential;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ @brief A string constant identifying the Google identity provider.
+ */
+extern NSString *const FIRGoogleAuthProviderID FIR_SWIFT_NAME(GoogleAuthProviderID);
+
+/** @class FIRGoogleAuthProvider
+ @brief Utility class for constructing Google Sign In credentials.
+ */
+FIR_SWIFT_NAME(GoogleAuthProvider)
+@interface FIRGoogleAuthProvider : NSObject
+
+/** @fn credentialWithIDToken:accessToken:
+ @brief Creates an @c FIRAuthCredential for a Google sign in.
+
+ @param IDToken The ID Token from Google.
+ @param accessToken The Access Token from Google.
+ @return A FIRAuthCredential containing the Google credentials.
+ */
++ (FIRAuthCredential *)credentialWithIDToken:(NSString *)IDToken
+ accessToken:(NSString *)accessToken;
+
+/** @fn init
+ @brief This class should not be initialized.
+ */
+- (instancetype)init NS_UNAVAILABLE;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/AuthProviders/Google/FIRGoogleAuthProvider.m b/Firebase/Auth/Source/AuthProviders/Google/FIRGoogleAuthProvider.m
new file mode 100644
index 0000000..a2f4c79
--- /dev/null
+++ b/Firebase/Auth/Source/AuthProviders/Google/FIRGoogleAuthProvider.m
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRGoogleAuthProvider.h"
+
+#import "FIRGoogleAuthCredential.h"
+#import "FIRAuthExceptionUtils.h"
+
+// FIRGoogleAuthProviderID is defined in FIRAuthProvider.m.
+
+@implementation FIRGoogleAuthProvider
+
+- (instancetype)init {
+ [FIRAuthExceptionUtils raiseMethodNotImplementedExceptionWithReason:
+ @"This class is not meant to be initialized."];
+ return nil;
+}
+
++ (FIRAuthCredential *)credentialWithIDToken:(NSString *)IDToken
+ accessToken:(NSString *)accessToken {
+ return [[FIRGoogleAuthCredential alloc] initWithIDToken:IDToken accessToken:accessToken];
+}
+
+@end
diff --git a/Firebase/Auth/Source/AuthProviders/OAuth/FIROAuthCredential.h b/Firebase/Auth/Source/AuthProviders/OAuth/FIROAuthCredential.h
new file mode 100644
index 0000000..744df33
--- /dev/null
+++ b/Firebase/Auth/Source/AuthProviders/OAuth/FIROAuthCredential.h
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+#import "../../Private/FIRAuthCredential_Internal.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @class FIROAuthCredential
+ @brief Internal implementation of FIRAuthCredential for generic credentials.
+ */
+@interface FIROAuthCredential : FIRAuthCredential
+
+/** @property providerID
+ @brief The provider ID associated with this credential.
+ */
+@property(nonatomic, readonly) NSString *providerID;
+
+/** @property IDToken
+ @brief The ID Token associated with this credential.
+ */
+@property(nonatomic, readonly) NSString *IDToken;
+
+/** @property accessToken
+ @brief The access token associated with this credential.
+ */
+@property(nonatomic, readonly) NSString *accessToken;
+
+/** @fn initWithProviderId:IDToken:accessToken:
+ @brief Designated initializer.
+ @param providerID The provider ID associated with the credential being created.
+ @param IDToken The ID Token associated with the credential being created.
+ @param accessToken The access token associated with the credential being created.
+ */
+- (nullable instancetype)initWithProvierID:(NSString *)providerID
+ IDToken:(nullable NSString*)IDToken
+ accessToken:(nullable NSString *)accessToken;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/AuthProviders/OAuth/FIROAuthCredential.m b/Firebase/Auth/Source/AuthProviders/OAuth/FIROAuthCredential.m
new file mode 100644
index 0000000..28712a6
--- /dev/null
+++ b/Firebase/Auth/Source/AuthProviders/OAuth/FIROAuthCredential.m
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIROAuthCredential.h"
+#import "FIRAuthExceptionUtils.h"
+#import "FIRVerifyAssertionRequest.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FIROAuthCredential ()
+
+- (nullable instancetype)initWithProvider:(NSString *)provider NS_UNAVAILABLE;
+
+@end
+
+@implementation FIROAuthCredential
+
+- (nullable instancetype)initWithProvierID:(NSString *)providerID
+ IDToken:(nullable NSString *)IDToken
+ accessToken:(nullable NSString *)accessToken {
+ self = [super initWithProvider:providerID];
+ if (self) {
+ _providerID = providerID;
+ _IDToken = IDToken;
+ _accessToken = accessToken;
+ }
+ return self;
+}
+
+- (void)prepareVerifyAssertionRequest:(FIRVerifyAssertionRequest *)request {
+ request.providerIDToken = _IDToken;
+ request.providerAccessToken = _accessToken;
+}
+
+NS_ASSUME_NONNULL_END
+
+@end
diff --git a/Firebase/Auth/Source/AuthProviders/OAuth/FIROAuthProvider.h b/Firebase/Auth/Source/AuthProviders/OAuth/FIROAuthProvider.h
new file mode 100644
index 0000000..e059b22
--- /dev/null
+++ b/Firebase/Auth/Source/AuthProviders/OAuth/FIROAuthProvider.h
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+#import "FIRAuthSwiftNameSupport.h"
+
+@class FIRAuthCredential;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @class FIROAuthProvider
+ @brief A concrete implementation of @c FIRAuthProvider for generic OAuth Providers.
+ */
+FIR_SWIFT_NAME(OAuthProvider)
+@interface FIROAuthProvider : NSObject
+
+/** @fn credentialWithProviderID:IDToken:accessToken:
+ @brief Creates an @c FIRAuthCredential for that OAuth 2 provider identified by providerID, ID
+ token and access token.
+
+ @param providerID The provider ID associated with the Auth credential being created.
+ @param IDToken The IDToken associated with the Auth credential being created.
+ @param accessToken The accessstoken associated with the Auth credential be created, if
+ available.
+ @return A FIRAuthCredential for the specified provider ID, ID token and access token.
+ */
++ (FIRAuthCredential *)credentialWithProviderID:(NSString *)providerID
+ IDToken:(NSString *)IDToken
+ accessToken:(nullable NSString *)accessToken;
+
+
+/** @fn credentialWithProviderID:accessToken:
+ @brief Creates an @c FIRAuthCredential for that OAuth 2 provider identified by providerID using
+ an ID token.
+
+ @param providerID The provider ID associated with the Auth credential being created.
+ @param accessToken The accessstoken associated with the Auth credential be created
+ @return A FIRAuthCredential.
+ */
++ (FIRAuthCredential *)credentialWithProviderID:(NSString *)providerID
+ accessToken:(NSString *)accessToken;
+
+/** @fn init
+ @brief This class is not meant to be initialized.
+ */
+- (instancetype)init NS_UNAVAILABLE;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/AuthProviders/OAuth/FIROAuthProvider.m b/Firebase/Auth/Source/AuthProviders/OAuth/FIROAuthProvider.m
new file mode 100644
index 0000000..e810680
--- /dev/null
+++ b/Firebase/Auth/Source/AuthProviders/OAuth/FIROAuthProvider.m
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIROAuthProvider.h"
+
+#import "FIROAuthCredential.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@implementation FIROAuthProvider
+
++ (FIRAuthCredential *)credentialWithProviderID:(NSString *)providerID
+ IDToken:(NSString *)IDToken
+ accessToken:(nullable NSString *)accessToken {
+ return [[FIROAuthCredential alloc] initWithProvierID:providerID
+ IDToken:IDToken
+ accessToken:accessToken];
+}
+
++ (FIROAuthCredential *)credentialWithProviderID:(NSString *)providerID
+ accessToken:(NSString *)accessToken {
+ return [[FIROAuthCredential alloc] initWithProvierID:providerID
+ IDToken:nil
+ accessToken:accessToken];
+}
+
+NS_ASSUME_NONNULL_END
+
+@end
diff --git a/Firebase/Auth/Source/AuthProviders/Phone/FIRPhoneAuthCredential.h b/Firebase/Auth/Source/AuthProviders/Phone/FIRPhoneAuthCredential.h
new file mode 100644
index 0000000..d951564
--- /dev/null
+++ b/Firebase/Auth/Source/AuthProviders/Phone/FIRPhoneAuthCredential.h
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+#import "FIRAuthCredential.h"
+#import "FIRAuthSwiftNameSupport.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @class FIRPhoneAuthCredential
+ @brief Implementation of FIRAuthCredential for Phone Auth credentials.
+ */
+FIR_SWIFT_NAME(PhoneAuthCredential)
+@interface FIRPhoneAuthCredential : FIRAuthCredential
+
+/** @fn init
+ @brief This class is not supposed to be instantiated directly.
+ */
+- (instancetype)init NS_UNAVAILABLE;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/AuthProviders/Phone/FIRPhoneAuthCredential.m b/Firebase/Auth/Source/AuthProviders/Phone/FIRPhoneAuthCredential.m
new file mode 100644
index 0000000..b9bf577
--- /dev/null
+++ b/Firebase/Auth/Source/AuthProviders/Phone/FIRPhoneAuthCredential.m
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRPhoneAuthCredential.h"
+
+#import "FIRPhoneAuthProvider.h"
+#import "FIRPhoneAuthCredential_Internal.h"
+#import "FIRAuthCredential_Internal.h"
+#import "FIRAuthExceptionUtils.h"
+#import "FIRVerifyAssertionRequest.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FIRPhoneAuthCredential ()
+
+- (nullable instancetype)initWithProvider:(NSString *)provider NS_UNAVAILABLE;
+
+@end
+
+@implementation FIRPhoneAuthCredential
+
+- (instancetype)initWithTemporaryProof:(NSString *)temporaryProof
+ phoneNumber:(NSString *)phoneNumber
+ providerID:(NSString *)providerID {
+ self = [super initWithProvider:providerID];
+ if (self) {
+ _temporaryProof = [temporaryProof copy];
+ _phoneNumber = [phoneNumber copy];
+ }
+ return self;
+}
+
+- (nullable instancetype)initWithProvider:(NSString *)provider {
+ [FIRAuthExceptionUtils raiseMethodNotImplementedExceptionWithReason:
+ @"Please call the designated initializer."];
+ return nil;
+}
+
+- (instancetype)initWithProviderID:(NSString *)providerID
+ verificationID:(NSString *)verificationID
+ verificationCode:(NSString *)verificationCode {
+ self = [super initWithProvider:providerID];
+ if (self) {
+ _verificationID = [verificationID copy];
+ _verificationCode = [verificationCode copy];
+ }
+ return self;
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/AuthProviders/Phone/FIRPhoneAuthCredential_Internal.h b/Firebase/Auth/Source/AuthProviders/Phone/FIRPhoneAuthCredential_Internal.h
new file mode 100644
index 0000000..f260b89
--- /dev/null
+++ b/Firebase/Auth/Source/AuthProviders/Phone/FIRPhoneAuthCredential_Internal.h
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+#import <Foundation/Foundation.h>
+
+#import "FIRPhoneAuthCredential.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @extension FIRPhoneAuthCredential
+ @brief Internal implementation of FIRAuthCredential for Phone Auth credentials.
+ */
+@interface FIRPhoneAuthCredential ()
+
+/** @var verificationID
+ @brief The verification ID obtained from invoking @c verifyPhoneNumber:completion:
+ */
+@property(nonatomic, readonly, nonnull) NSString *verificationID;
+
+/** @var verificationCode
+ @brief The verification code provided by the user.
+ */
+@property(nonatomic, readonly, nonnull) NSString *verificationCode;
+
+/** @var temporaryProof
+ @brief The a temporary proof code perftaining to this credential, returned from the backend.
+ */
+@property(nonatomic, readonly, nonnull) NSString *temporaryProof;
+
+/** @var phoneNumber
+ @brief The a phone number pertaining to this credential, returned from the backend.
+ */
+@property(nonatomic, readonly, nonnull) NSString *phoneNumber;
+
+/** @var initWithTemporaryProof:phoneNumber:
+ @brief Designated Initializer.
+ @param providerID The provider ID associated with the phone auth credential being created.
+ */
+- (instancetype)initWithTemporaryProof:(NSString *)temporaryProof
+ phoneNumber:(NSString *)phoneNumber
+ providerID:(NSString *)providerID NS_DESIGNATED_INITIALIZER;
+
+/** @var initWithProviderID:verificationID:verificationCode:
+ @brief Designated Initializer.
+ @param providerID The provider ID associated with the phone auth credential being created.
+ @param verificationID The verification ID associated witht Phone Auth credential being created.
+ @param verificationCode The verification code associated witht Phone Auth credential being
+ created.
+ */
+- (instancetype)initWithProviderID:(NSString *)providerID
+ verificationID:(NSString *)verificationID
+ verificationCode:(NSString *)verificationCode NS_DESIGNATED_INITIALIZER;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/AuthProviders/Phone/FIRPhoneAuthProvider.h b/Firebase/Auth/Source/AuthProviders/Phone/FIRPhoneAuthProvider.h
new file mode 100644
index 0000000..bc12b43
--- /dev/null
+++ b/Firebase/Auth/Source/AuthProviders/Phone/FIRPhoneAuthProvider.h
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+#import "FIRAuth.h"
+#import "FIRAuthSwiftNameSupport.h"
+
+@class FIRPhoneAuthCredential;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @var FIRPhoneAuthProviderID
+ @brief A string constant identifying the phone identity provider.
+ */
+extern NSString *const FIRPhoneAuthProviderID FIR_SWIFT_NAME(PhoneAuthProviderID);
+
+/** @typedef FIRVerificationResultCallback
+ @brief The type of block invoked when a request to send a verification code has finished.
+
+ @param verificationID On success, the verification ID provided, nil otherwise.
+ @param error On error, the error that occured, nil otherwise.
+ */
+typedef void (^FIRVerificationResultCallback)(NSString *_Nullable verificationID,
+ NSError *_Nullable error)
+ FIR_SWIFT_NAME(VerificationResultCallback);
+
+/** @class FIRPhoneNumberProvider
+ @brief A concrete implementation of @c FIRAuthProvider for Phone Auth Providers.
+ */
+FIR_SWIFT_NAME(PhoneAuthProvider)
+@interface FIRPhoneAuthProvider : NSObject
+
+/** @fn provider
+ @brief Returns an instance of @c FIRPhoneAuthProvider for the default @c FIRAuth object.
+ */
++ (instancetype)provider FIR_SWIFT_NAME(provider());
+
+/** @fn providerWithAuth:
+ @brief Returns an instance of @c FIRPhoneAuthProvider for the provided @c FIRAuth object.
+
+ @param auth The auth object to associate with the @c PhoneauthProvider instance.
+ */
++ (instancetype)providerWithAuth:(FIRAuth *)auth FIR_SWIFT_NAME(provider(auth:));
+
+/** @fn verifyPhoneNumber:completion:
+ @brief Starts the phone number authentication flow by sending a verifcation code to the
+ specified phone number.
+
+ @param phoneNumber The phone number to be verified.
+ @param completion The callback to be invoked when the verification flow is finished.
+ */
+- (void)verifyPhoneNumber:(NSString *)phoneNumber
+ completion:(nullable FIRVerificationResultCallback)completion;
+
+/** @fn credentialWithVerificationID:verificationCode:
+ @brief Creates an @c FIRAuthCredential for the phone number provider identified by the
+ verification ID and verification code.
+
+ @param verificationID The verification ID obtained from invoking @c
+ verifyPhoneNumber:completion:
+ @param verificationCode The verification code obtained from the user.
+ @return The corresponding @c FIRAuthCredential for the verification ID and verification code
+ provided.
+ */
+- (FIRPhoneAuthCredential *)credentialWithVerificationID:(NSString *)verificationID
+ verificationCode:(NSString *)verificationCode;
+
+/** @fn init
+ @brief Please use the @c provider or @providerWithAuth: methods to obtain an instance of @c
+ FIRPhoneAuthProvider.
+ */
+- (instancetype)init NS_UNAVAILABLE;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/AuthProviders/Phone/FIRPhoneAuthProvider.m b/Firebase/Auth/Source/AuthProviders/Phone/FIRPhoneAuthProvider.m
new file mode 100644
index 0000000..423b2b6
--- /dev/null
+++ b/Firebase/Auth/Source/AuthProviders/Phone/FIRPhoneAuthProvider.m
@@ -0,0 +1,213 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRPhoneAuthProvider.h"
+
+#import "FIRLogger.h"
+#import "FIRPhoneAuthCredential_Internal.h"
+#import "NSString+FIRAuth.h"
+#import "../../Private/FIRAuthAPNSToken.h"
+#import "../../Private/FIRAuthAPNSTokenManager.h"
+#import "../../Private/FIRAuthAppCredential.h"
+#import "../../Private/FIRAuthAppCredentialManager.h"
+#import "../../Private/FIRAuthGlobalWorkQueue.h"
+#import "../../Private/FIRAuth_Internal.h"
+#import "../../Private/FIRAuthNotificationManager.h"
+#import "../../Private/FIRAuthErrorUtils.h"
+#import "FIRAuthBackend.h"
+#import "FIRSendVerificationCodeRequest.h"
+#import "FIRSendVerificationCodeResponse.h"
+#import "FIRVerifyClientRequest.h"
+#import "FIRVerifyClientResponse.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @typedef FIRVerifyClientCallback
+ @brief The callback invoked at the end of a client verification flow.
+ @param appCredential credential that proves the identity of the app during a phone
+ authentication flow.
+ @param error The error that occured while verifying the app, if any.
+ */
+typedef void (^FIRVerifyClientCallback)(FIRAuthAppCredential *_Nullable appCredential,
+ NSError *_Nullable error);
+
+@implementation FIRPhoneAuthProvider {
+
+ /** @var _auth
+ @brief The auth instance used to for verifying the phone number.
+ */
+ FIRAuth *_auth;
+}
+
+/** @fn initWithAuth:
+ @brief returns an instance of @c FIRPhoneAuthProvider assocaited with the provided auth
+ instance.
+ @return An Instance of @c FIRPhoneAuthProvider.
+ */
+- (nullable instancetype)initWithAuth:(FIRAuth *)auth {
+ self = [super init];
+ if (self) {
+ _auth = auth;
+ }
+ return self;
+}
+
+- (void)verifyPhoneNumber:(NSString *)phoneNumber
+ completion:(nullable FIRVerificationResultCallback)completion {
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^{
+ FIRVerificationResultCallback callBackOnMainThread = ^(NSString *_Nullable verificationID,
+ NSError *_Nullable error) {
+ if (completion) {
+ dispatch_async(dispatch_get_main_queue(), ^{
+ completion(verificationID, error);
+ });
+ }
+ };
+
+ if (!phoneNumber.length) {
+ callBackOnMainThread(nil,
+ [FIRAuthErrorUtils missingPhoneNumberErrorWithMessage:nil]);
+ return;
+ }
+ [_auth.notificationManager checkNotificationForwardingWithCallback:
+ ^(BOOL isNotificationBeingForwarded) {
+ if (!isNotificationBeingForwarded) {
+ callBackOnMainThread(nil, [FIRAuthErrorUtils notificationNotForwardedError]);
+ return;
+ }
+ [self verifyClientAndSendVerificationCodeToPhoneNumber:phoneNumber
+ retryOnInvalidAppCredential:YES
+ callback:callBackOnMainThread];
+ }];
+ });
+}
+
+- (FIRPhoneAuthCredential *)credentialWithVerificationID:(NSString *)verificationID
+ verificationCode:(NSString *)verificationCode {
+ return [[FIRPhoneAuthCredential alloc] initWithProviderID:FIRPhoneAuthProviderID
+ verificationID:verificationID
+ verificationCode:verificationCode];
+}
+
++ (instancetype)provider {
+ return [[self alloc]initWithAuth:[FIRAuth auth]];
+}
+
++ (instancetype)providerWithAuth:(FIRAuth *)auth {
+ return [[self alloc]initWithAuth:auth];
+}
+
+#pragma mark - Internal Methods
+
+/** @fn verifyClientAndSendVerificationCodeToPhoneNumber:retryOnInvalidAppCredential:callback:
+ @brief Starts the flow to verify the client via silent push notification.
+ @param retryOnInvalidAppCredential Whether of not the flow should be retried if an
+ FIRAuthErrorCodeInvalidAppCredential error is returned from the backend.
+ @param phoneNumber The phone number to be verified.
+ @param callback The callback to be invoked on the global work queue when the flow is
+ finished.
+ */
+- (void)verifyClientAndSendVerificationCodeToPhoneNumber:(NSString *)phoneNumber
+ retryOnInvalidAppCredential:(BOOL)retryOnInvalidAppCredential
+ callback:(FIRVerificationResultCallback)callback {
+ [self verifyClientWithCompletion:^(FIRAuthAppCredential *_Nullable appCredential,
+ NSError *_Nullable error) {
+ if (error) {
+ callback(nil, error);
+ return;
+ }
+ FIRSendVerificationCodeRequest *request =
+ [[FIRSendVerificationCodeRequest alloc] initWithPhoneNumber:phoneNumber
+ appCredential:appCredential
+ APIKey:_auth.APIKey];
+ [FIRAuthBackend sendVerificationCode:request
+ callback:^(FIRSendVerificationCodeResponse *_Nullable response,
+ NSError *_Nullable error) {
+ if (error) {
+ if (error.code == FIRAuthErrorCodeInvalidAppCredential) {
+ if (retryOnInvalidAppCredential) {
+ [_auth.appCredentialManager clearCredential];
+ [self verifyClientAndSendVerificationCodeToPhoneNumber:phoneNumber
+ retryOnInvalidAppCredential:NO
+ callback:callback];
+ return;
+ }
+ callback(nil, [FIRAuthErrorUtils unexpectedResponseWithDeserializedResponse:nil
+ underlyingError:error]);
+ return;
+ }
+ callback(nil, error);
+ return;
+ }
+ // Associate the phone number with the verification ID.
+ response.verificationID.fir_authPhoneNumber = phoneNumber;
+ callback(response.verificationID, nil);
+ }];
+ }];
+}
+
+/** @fn verifyClientWithCompletion:completion:
+ @brief Continues the flow to verify the client via silent push notification.
+ @param completion The callback to be invoked when the client verification flow is finished.
+ */
+- (void)verifyClientWithCompletion:(FIRVerifyClientCallback)completion {
+ if (_auth.appCredentialManager.credential) {
+ completion(_auth.appCredentialManager.credential, nil);
+ return;
+ }
+ [_auth.tokenManager getTokenWithCallback:^(FIRAuthAPNSToken * _Nullable token) {
+ if (!token) {
+ completion(nil, [FIRAuthErrorUtils missingAppTokenError]);
+ return;
+ }
+
+ // Convert token data to hex string.
+ NSUInteger capacity = token.data.length * 2;
+ NSMutableString *tokenString = [NSMutableString stringWithCapacity:capacity];
+ const unsigned char *tokenData = token.data.bytes;
+ for (int idx = 0; idx < token.data.length; ++idx) {
+ [tokenString appendFormat:@"%02X", (int)tokenData[idx]];
+ }
+
+ FIRVerifyClientRequest *request =
+ [[FIRVerifyClientRequest alloc] initWithAppToken:tokenString
+ isSandbox:token.type == FIRAuthAPNSTokenTypeSandbox
+ APIKey:_auth.APIKey];
+ [FIRAuthBackend verifyClient:request callback:^(FIRVerifyClientResponse *_Nullable response,
+ NSError *_Nullable error) {
+ if (error) {
+ completion(nil, error);
+ return;
+ }
+ NSTimeInterval timeout = [response.suggestedTimeOutDate timeIntervalSinceNow];
+ [_auth.appCredentialManager
+ didStartVerificationWithReceipt:response.receipt
+ timeout:timeout
+ callback:^(FIRAuthAppCredential *credential) {
+ if (!credential.secret) {
+ FIRLogError(kFIRLoggerAuth, @"I-AUT000014",
+ @"Failed to receive remote notification to verify app identity within "
+ @"%.0f second(s)", timeout);
+ }
+ completion(credential, nil);
+ }];
+ }];
+ }];
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/AuthProviders/Phone/NSString+FIRAuth.h b/Firebase/Auth/Source/AuthProviders/Phone/NSString+FIRAuth.h
new file mode 100644
index 0000000..ba123fa
--- /dev/null
+++ b/Firebase/Auth/Source/AuthProviders/Phone/NSString+FIRAuth.h
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @category NSString(FIRAuth)
+ @brief A FIRAuth category for extending the functionality of NSString for specific Firebase Auth
+ use cases.
+ */
+@interface NSString (FIRAuth)
+
+/** @property fir_authPhoneNumber
+ @brief A phone number associated with the verification ID (NSString instance).
+ @remarks Allows an instance on NSString to be associated with a phone number in order to link
+ phone number with the verificationID returned from verifyPhoneNumber:completion:
+ */
+@property(nonatomic, strong) NSString *fir_authPhoneNumber;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/AuthProviders/Phone/NSString+FIRAuth.m b/Firebase/Auth/Source/AuthProviders/Phone/NSString+FIRAuth.m
new file mode 100644
index 0000000..87f3b1c
--- /dev/null
+++ b/Firebase/Auth/Source/AuthProviders/Phone/NSString+FIRAuth.m
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "NSString+FIRAuth.h"
+
+#import <objc/runtime.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+@implementation NSString (FIRAuth)
+
+- (void)setFir_authPhoneNumber:(NSString *)phoneNumber {
+ objc_setAssociatedObject(self, @selector(fir_authPhoneNumber), phoneNumber,
+ OBJC_ASSOCIATION_RETAIN_NONATOMIC);
+}
+
+- (NSString *)fir_authPhoneNumber {
+ return objc_getAssociatedObject(self, @selector(fir_authPhoneNumber));
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/AuthProviders/Twitter/FIRTwitterAuthCredential.h b/Firebase/Auth/Source/AuthProviders/Twitter/FIRTwitterAuthCredential.h
new file mode 100644
index 0000000..5fab4e2
--- /dev/null
+++ b/Firebase/Auth/Source/AuthProviders/Twitter/FIRTwitterAuthCredential.h
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+#import "../../Private/FIRAuthCredential_Internal.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @class FIRTwitterAuthCredential
+ @brief Internal implementation of FIRAuthCredential for Twitter credentials.
+ */
+@interface FIRTwitterAuthCredential : FIRAuthCredential
+
+/** @property token
+ @brief The Twitter OAuth token.
+ */
+@property(nonatomic, readonly) NSString *token;
+
+/** @property secret
+ @brief The Twitter OAuth secret.
+ */
+@property(nonatomic, readonly) NSString *secret;
+
+/** @fn initWithToken:secret:
+ @brief Designated initializer.
+ @param token The Twitter OAuth token.
+ @param secret The Twitter OAuth secret.
+ */
+- (nullable instancetype)initWithToken:(NSString *)token secret:(NSString *)secret
+ NS_DESIGNATED_INITIALIZER;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/AuthProviders/Twitter/FIRTwitterAuthCredential.m b/Firebase/Auth/Source/AuthProviders/Twitter/FIRTwitterAuthCredential.m
new file mode 100644
index 0000000..6772d6f
--- /dev/null
+++ b/Firebase/Auth/Source/AuthProviders/Twitter/FIRTwitterAuthCredential.m
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRTwitterAuthCredential.h"
+
+#import "FIRTwitterAuthProvider.h"
+#import "FIRAuthExceptionUtils.h"
+#import "FIRVerifyAssertionRequest.h"
+
+@interface FIRTwitterAuthCredential ()
+
+- (nullable instancetype)initWithProvider:(NSString *)provider NS_UNAVAILABLE;
+
+@end
+
+@implementation FIRTwitterAuthCredential
+
+- (nullable instancetype)initWithProvider:(NSString *)provider {
+ [FIRAuthExceptionUtils raiseMethodNotImplementedExceptionWithReason:
+ @"Please call the designated initializer."];
+ return nil;
+}
+
+- (nullable instancetype)initWithToken:(NSString *)token secret:(NSString *)secret {
+ self = [super initWithProvider:FIRTwitterAuthProviderID];
+ if (self) {
+ _token = [token copy];
+ _secret = [secret copy];
+ }
+ return self;
+}
+
+- (void)prepareVerifyAssertionRequest:(FIRVerifyAssertionRequest *)request {
+ request.providerAccessToken = _token;
+ request.providerOAuthTokenSecret = _secret;
+}
+
+@end
diff --git a/Firebase/Auth/Source/AuthProviders/Twitter/FIRTwitterAuthProvider.h b/Firebase/Auth/Source/AuthProviders/Twitter/FIRTwitterAuthProvider.h
new file mode 100644
index 0000000..d8f647d
--- /dev/null
+++ b/Firebase/Auth/Source/AuthProviders/Twitter/FIRTwitterAuthProvider.h
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+#import "FIRAuthSwiftNameSupport.h"
+
+@class FIRAuthCredential;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ @brief A string constant identifying the Twitter identity provider.
+ */
+extern NSString *const FIRTwitterAuthProviderID FIR_SWIFT_NAME(TwitterAuthProviderID);
+
+/** @class FIRTwitterAuthProvider
+ @brief Utility class for constructing Twitter credentials.
+ */
+FIR_SWIFT_NAME(TwitterAuthProvider)
+@interface FIRTwitterAuthProvider : NSObject
+
+/** @fn credentialWithToken:secret:
+ @brief Creates an @c FIRAuthCredential for a Twitter sign in.
+
+ @param token The Twitter OAuth token.
+ @param secret The Twitter OAuth secret.
+ @return A FIRAuthCredential containing the Twitter credential.
+ */
++ (FIRAuthCredential *)credentialWithToken:(NSString *)token secret:(NSString *)secret;
+
+/** @fn init
+ @brief This class is not meant to be initialized.
+ */
+- (instancetype)init NS_UNAVAILABLE;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/AuthProviders/Twitter/FIRTwitterAuthProvider.m b/Firebase/Auth/Source/AuthProviders/Twitter/FIRTwitterAuthProvider.m
new file mode 100644
index 0000000..5d738ce
--- /dev/null
+++ b/Firebase/Auth/Source/AuthProviders/Twitter/FIRTwitterAuthProvider.m
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRTwitterAuthProvider.h"
+
+#import "FIRTwitterAuthCredential.h"
+#import "FIRAuthExceptionUtils.h"
+
+// FIRTwitterAuthProviderID is defined in FIRAuthProvider.m.
+
+@implementation FIRTwitterAuthProvider
+
+- (instancetype)init {
+ [FIRAuthExceptionUtils raiseMethodNotImplementedExceptionWithReason:
+ @"This class is not meant to be initialized."];
+ return nil;
+}
+
++ (FIRAuthCredential *)credentialWithToken:(NSString *)token secret:(NSString *)secret {
+ return [[FIRTwitterAuthCredential alloc] initWithToken:token secret:secret];
+}
+
+@end
diff --git a/Firebase/Auth/Source/FIRActionCodeSettings.m b/Firebase/Auth/Source/FIRActionCodeSettings.m
new file mode 100644
index 0000000..26d7538
--- /dev/null
+++ b/Firebase/Auth/Source/FIRActionCodeSettings.m
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "Private/FIRActionCodeSettings.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@implementation FIRActionCodeSettings
+
+- (void)setIOSBundleID:(NSString *)iOSBundleID
+ appStoreID:(nullable NSString *)appStoreID {
+ _iOSBundleID = [iOSBundleID copy];
+ _iOSAppStoreID = [appStoreID copy];
+}
+
+- (void)setAndroidPackageName:(NSString *)androidPackageName
+ installIfNotAvailable:(BOOL)installIfNotAvailable
+ minimumVersion:(nullable NSString *)minimumVersion {
+ _androidPackageName = [androidPackageName copy];
+ _androidInstallIfNotAvailable = installIfNotAvailable;
+ _androidMinimumVersion = [minimumVersion copy];
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/FIRAdditionalUserInfo.h b/Firebase/Auth/Source/FIRAdditionalUserInfo.h
new file mode 100644
index 0000000..70e9e57
--- /dev/null
+++ b/Firebase/Auth/Source/FIRAdditionalUserInfo.h
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+#import "FIRAuthSwiftNameSupport.h"
+
+@class FIRVerifyAssertionResponse;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @class FIRAdditionalUserInfo
+ @brief Represents additional user data returned from an identity provider.
+ */
+FIR_SWIFT_NAME(AdditionalUserInfo)
+@interface FIRAdditionalUserInfo : NSObject
+
+/** @fn init
+ @brief This class should not be initialized manually. @c FIRAdditionalUserInfo can be retrieved
+ from @c FIRAuthDataResult .
+ */
+- (instancetype)init NS_UNAVAILABLE;
+
+/** @property providerID
+ @brief The provider identifier.
+ */
+@property(nonatomic, readonly) NSString *providerID;
+
+/** @property profile
+ @brief profile Dictionary containing the additional IdP specific information.
+ */
+@property(nonatomic, readonly, nullable) NSDictionary<NSString *, NSObject *> *profile;
+
+/** @property username
+ @brief username The name of the user.
+ */
+@property(nonatomic, readonly, nullable) NSString *username;
+
+/** @property newUser
+ @brief Indicates whether or not the current user was signed in for the first time.
+ */
+@property(nonatomic, readonly, getter=isNewUser) BOOL newUser;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/FIRAdditionalUserInfo.m b/Firebase/Auth/Source/FIRAdditionalUserInfo.m
new file mode 100644
index 0000000..e00347d
--- /dev/null
+++ b/Firebase/Auth/Source/FIRAdditionalUserInfo.m
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "Private/FIRAdditionalUserInfo_Internal.h"
+
+#import "FIRVerifyAssertionResponse.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@implementation FIRAdditionalUserInfo
+
+/** @var kProviderIDCodingKey
+ @brief The key used to encode the providerID property for NSSecureCoding.
+ */
+static NSString *const kProviderIDCodingKey = @"providerID";
+
+/** @var kProfileCodingKey
+ @brief The key used to encode the profile property for NSSecureCoding.
+ */
+static NSString *const kProfileCodingKey = @"profile";
+
+/** @var kUsernameCodingKey
+ @brief The key used to encode the username property for NSSecureCoding.
+ */
+static NSString *const kUsernameCodingKey = @"username";
+
+/** @var kNewUserKey
+ @brief The key used to encode the newUser property for NSSecureCoding.
+ */
+static NSString *const kNewUserKey = @"newUser";
+
++ (nullable instancetype)userInfoWithVerifyAssertionResponse:
+ (FIRVerifyAssertionResponse *)verifyAssertionResponse {
+ return [[self alloc] initWithProviderID:verifyAssertionResponse.providerID
+ profile:verifyAssertionResponse.profile
+ username:verifyAssertionResponse.username
+ isNewUser:verifyAssertionResponse.isNewUser];
+}
+
+- (nullable instancetype)initWithProviderID:(NSString *)providerID
+ profile:(nullable NSDictionary<NSString *, NSObject *> *)profile
+ username:(nullable NSString *)username
+ isNewUser:(BOOL)isNewUser {
+ self = [super init];
+ if (self) {
+ _providerID = [providerID copy];
+ if (profile) {
+ _profile = [[NSDictionary alloc] initWithDictionary:profile copyItems:YES];
+ }
+ _username = [username copy];
+ _newUser = isNewUser;
+ }
+ return self;
+}
+
+#pragma mark - NSSecureCoding
+
++ (BOOL)supportsSecureCoding {
+ return YES;
+}
+
+- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder {
+ NSString *providerID =
+ [aDecoder decodeObjectOfClass:[NSString class] forKey:kProviderIDCodingKey];
+ NSDictionary<NSString *, NSObject *> *profile =
+ [aDecoder decodeObjectOfClass:[NSDictionary class] forKey:kProfileCodingKey];
+ NSString *username = [aDecoder decodeObjectOfClass:[NSString class] forKey:kUsernameCodingKey];
+ NSNumber *isNewUser = [aDecoder decodeObjectOfClass:[NSNumber class] forKey:kNewUserKey];
+
+ return [self initWithProviderID:providerID
+ profile:profile
+ username:username
+ isNewUser:isNewUser.boolValue];
+}
+
+- (void)encodeWithCoder:(NSCoder *)aCoder {
+ [aCoder encodeObject:_providerID forKey:kProviderIDCodingKey];
+ [aCoder encodeObject:_profile forKey:kProfileCodingKey];
+ [aCoder encodeObject:_username forKey:kUsernameCodingKey];
+ [aCoder encodeObject:[NSNumber numberWithBool:_newUser] forKey:kNewUserKey];
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/FIRAuth.h b/Firebase/Auth/Source/FIRAuth.h
new file mode 100644
index 0000000..b913380
--- /dev/null
+++ b/Firebase/Auth/Source/FIRAuth.h
@@ -0,0 +1,612 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+#import "FIRAuthAPNSTokenType.h"
+#import "FIRAuthErrors.h"
+#import "FIRAuthSwiftNameSupport.h"
+
+@class FIRApp;
+@class FIRAuth;
+@class FIRAuthCredential;
+@class FIRAuthDataResult;
+@class FIRUser;
+@protocol FIRAuthStateListener;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @typedef FIRAuthStateDidChangeListenerHandle
+ @brief The type of handle returned by @c FIRAuth.addAuthStateDidChangeListener:.
+ */
+typedef id<NSObject> FIRAuthStateDidChangeListenerHandle
+ FIR_SWIFT_NAME(AuthStateDidChangeListenerHandle);
+
+/** @typedef FIRAuthStateDidChangeListenerBlock
+ @brief The type of block which can be registered as a listener for auth state did change events.
+
+ @param auth The FIRAuth object on which state changes occurred.
+ @param user Optionally; the current signed in user, if any.
+ */
+typedef void(^FIRAuthStateDidChangeListenerBlock)(FIRAuth *auth, FIRUser *_Nullable user)
+ FIR_SWIFT_NAME(AuthStateDidChangeListenerBlock);
+
+/** @typedef FIRIDTokenDidChangeListenerHandle
+ @brief The type of handle returned by @c FIRAuth.addIDTokenDidChangeListener:.
+ */
+typedef id<NSObject> FIRIDTokenDidChangeListenerHandle
+ FIR_SWIFT_NAME(IDTokenDidChangeListenerHandle);
+
+/** @typedef FIRIDTokenDidChangeListenerBlock
+ @brief The type of block which can be registered as a listener for ID token did change events.
+
+ @param auth The FIRAuth object on which ID token changes occurred.
+ @param user Optionally; the current signed in user, if any.
+ */
+typedef void(^FIRIDTokenDidChangeListenerBlock)(FIRAuth *auth, FIRUser *_Nullable user)
+ FIR_SWIFT_NAME(IDTokenDidChangeListenerBlock);
+
+/** @typedef FIRAuthDataResultCallback
+ @brief The type of block invoked when sign-in related events complete.
+
+ @param authResult Optionally; Result of sign-in request containing @c FIRUser and
+ @c FIRAdditionalUserInfo.
+ @param error Optionally; the error which occurred - or nil if the request was successful.
+ */
+typedef void (^FIRAuthDataResultCallback)(FIRAuthDataResult *_Nullable authResult,
+ NSError *_Nullable error)
+ FIR_SWIFT_NAME(AuthDataResultCallback);
+
+#if defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0
+/**
+ @brief The name of the @c NSNotificationCenter notification which is posted when the auth state
+ changes (for example, a new token has been produced, a user signs in or signs out). The
+ object parameter of the notification is the sender @c FIRAuth instance.
+ */
+extern const NSNotificationName FIRAuthStateDidChangeNotification
+ FIR_SWIFT_NAME(AuthStateDidChange);
+#else
+/**
+ @brief The name of the @c NSNotificationCenter notification which is posted when the auth state
+ changes (for example, a new token has been produced, a user signs in or signs out). The
+ object parameter of the notification is the sender @c FIRAuth instance.
+ */
+extern NSString *const FIRAuthStateDidChangeNotification
+ FIR_SWIFT_NAME(AuthStateDidChangeNotification);
+#endif // defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0
+
+/** @typedef FIRAuthResultCallback
+ @brief The type of block invoked when sign-in related events complete.
+
+ @param user Optionally; the signed in user, if any.
+ @param error Optionally; if an error occurs, this is the NSError object that describes the
+ problem. Set to nil otherwise.
+ */
+typedef void (^FIRAuthResultCallback)(FIRUser *_Nullable user, NSError *_Nullable error)
+ FIR_SWIFT_NAME(AuthResultCallback);
+
+/** @typedef FIRProviderQueryCallback
+ @brief The type of block invoked when a list of identity providers for a given email address is
+ requested.
+
+ @param providers Optionally; a list of provider identifiers, if any.
+ @see FIRGoogleAuthProviderID etc.
+ @param error Optionally; if an error occurs, this is the NSError object that describes the
+ problem. Set to nil otherwise.
+ */
+typedef void (^FIRProviderQueryCallback)(NSArray<NSString *> *_Nullable providers,
+ NSError *_Nullable error)
+ FIR_SWIFT_NAME(ProviderQueryCallback);
+
+/** @typedef FIRSendPasswordResetCallback
+ @brief The type of block invoked when sending a password reset email.
+
+ @param error Optionally; if an error occurs, this is the NSError object that describes the
+ problem. Set to nil otherwise.
+ */
+typedef void (^FIRSendPasswordResetCallback)(NSError *_Nullable error)
+ FIR_SWIFT_NAME(SendPasswordResetCallback);
+
+/** @typedef FIRConfirmPasswordResetCallback
+ @brief The type of block invoked when performing a password reset.
+
+ @param error Optionally; if an error occurs, this is the NSError object that describes the
+ problem. Set to nil otherwise.
+ */
+typedef void (^FIRConfirmPasswordResetCallback)(NSError *_Nullable error)
+ FIR_SWIFT_NAME(ConfirmPasswordResetCallback);
+
+/** @typedef FIRVerifyPasswordResetCodeCallback
+ @brief The type of block invoked when verifying that an out of band code should be used to
+ perform password reset.
+
+ @param email Optionally; the email address of the user for which the out of band code applies.
+ @param error Optionally; if an error occurs, this is the NSError object that describes the
+ problem. Set to nil otherwise.
+ */
+typedef void (^FIRVerifyPasswordResetCodeCallback)(NSString *_Nullable email,
+ NSError *_Nullable error)
+ FIR_SWIFT_NAME(VerifyPasswordResetCodeCallback);
+
+/** @typedef FIRApplyActionCodeCallback
+ @brief The type of block invoked when applying an action code.
+
+ @param error Optionally; if an error occurs, this is the NSError object that describes the
+ problem. Set to nil otherwise.
+ */
+typedef void (^FIRApplyActionCodeCallback)(NSError *_Nullable error)
+ FIR_SWIFT_NAME(ApplyActionCodeCallback);
+
+/**
+ @brief Keys used to retrieve operation data from a @c FIRActionCodeInfo object by the @c
+ dataForKey method.
+ */
+typedef NS_ENUM(NSInteger, FIRActionDataKey) {
+ /**
+ * The email address to which the code was sent.
+ * For FIRActionCodeOperationRecoverEmail, the new email address for the account.
+ */
+ FIRActionCodeEmailKey = 0,
+
+ /** For FIRActionCodeOperationRecoverEmail, the current email address for the account. */
+ FIRActionCodeFromEmailKey = 1
+} FIR_SWIFT_NAME(ActionDataKey);
+
+/** @class FIRActionCodeInfo
+ @brief Manages information regarding action codes.
+ */
+FIR_SWIFT_NAME(ActionCodeInfo)
+@interface FIRActionCodeInfo : NSObject
+
+/**
+ @brief Operations which can be performed with action codes.
+ */
+typedef NS_ENUM(NSInteger, FIRActionCodeOperation) {
+ /** Action code for unknown operation. */
+ FIRActionCodeOperationUnknown = 0,
+
+ /** Action code for password reset operation. */
+ FIRActionCodeOperationPasswordReset = 1,
+
+ /** Action code for verify email operation. */
+ FIRActionCodeOperationVerifyEmail = 2
+} FIR_SWIFT_NAME(ActionCodeOperation);
+
+/**
+ @brief The operation being performed.
+ */
+@property(nonatomic, readonly) FIRActionCodeOperation operation;
+
+/** @fn dataForKey:
+ @brief The operation being performed.
+
+ @param key The FIRActionDataKey value used to retrieve the operation data.
+
+ @return The operation data pertaining to the provided action code key.
+ */
+- (NSString *)dataForKey:(FIRActionDataKey)key;
+
+/** @fn init
+ @brief please use initWithOperation: instead.
+ */
+- (instancetype)init NS_UNAVAILABLE;
+
+@end
+
+/** @typedef FIRCheckActionCodeCallBack
+ @brief The type of block invoked when performing a check action code operation.
+
+ @param info Metadata corresponding to the action code.
+ @param error Optionally; if an error occurs, this is the NSError object that describes the
+ problem. Set to nil otherwise.
+ */
+typedef void (^FIRCheckActionCodeCallBack)(FIRActionCodeInfo *_Nullable info,
+ NSError *_Nullable error)
+ FIR_SWIFT_NAME(CheckActionCodeCallback);
+
+/** @class FIRAuth
+ @brief Manages authentication for Firebase apps.
+ @remarks This class is thread-safe.
+ */
+FIR_SWIFT_NAME(Auth)
+@interface FIRAuth : NSObject
+
+/** @fn auth
+ @brief Gets the auth object for the default Firebase app.
+ @remarks The default Firebase app must have already been configured or an exception will be
+ raised.
+ */
++ (FIRAuth *)auth FIR_SWIFT_NAME(auth());
+
+/** @fn authWithApp:
+ @brief Gets the auth object for a @c FIRApp.
+
+ @param app The FIRApp for which to retrieve the associated FIRAuth instance.
+ @return The FIRAuth instance associated with the given FIRApp.
+ */
++ (FIRAuth *)authWithApp:(FIRApp *)app FIR_SWIFT_NAME(auth(app:));
+
+/** @property app
+ @brief Gets the @c FIRApp object that this auth object is connected to.
+ */
+@property(nonatomic, weak, readonly, nullable) FIRApp *app;
+
+/** @property currentUser
+ @brief Synchronously gets the cached current user, or null if there is none.
+ */
+@property(nonatomic, strong, readonly, nullable) FIRUser *currentUser;
+
+/** @property APNSToken
+ @brief The APNs token used for phone number authentication. The type of the token (production
+ or sandbox) will be attempted to be automatcially detected.
+ @remarks If swizzling is disabled, the APNs Token must be set for phone number auth to work,
+ by either setting this property or by calling @c setAPNSToken:type:
+ */
+@property(nonatomic, strong, nullable) NSData *APNSToken;
+
+/** @fn init
+ @brief Please access auth instances using @c FIRAuth.auth and @c FIRAuth.authForApp:.
+ */
+- (instancetype)init NS_UNAVAILABLE;
+
+/** @fn fetchProvidersForEmail:completion:
+ @brief Fetches the list of IdPs that can be used for signing in with the provided email address.
+ Useful for an "identifier-first" sign-in flow.
+
+ @param email The email address for which to obtain a list of identity providers.
+ @param completion Optionally; a block which is invoked when the list of providers for the
+ specified email address is ready or an error was encountered. Invoked asynchronously on the
+ main thread in the future.
+
+ @remarks Possible error codes:
+ <ul>
+ <li>@c FIRAuthErrorCodeInvalidEmail - Indicates the email address is malformed.</li>
+ </ul>
+
+ @remarks See @c FIRAuthErrors for a list of error codes that are common to all API methods.
+ */
+- (void)fetchProvidersForEmail:(NSString *)email
+ completion:(nullable FIRProviderQueryCallback)completion;
+
+/** @fn signInWithEmail:password:completion:
+ @brief Signs in using an email address and password.
+
+ @param email The user's email address.
+ @param password The user's password.
+ @param completion Optionally; a block which is invoked when the sign in flow finishes, or is
+ canceled. Invoked asynchronously on the main thread in the future.
+
+ @remarks Possible error codes:
+
+ <ul>
+ <li>@c FIRAuthErrorCodeOperationNotAllowed - Indicates that email and password
+ accounts are not enabled. Enable them in the Auth section of the
+ Firebase console.
+ </li>
+ <li>@c FIRAuthErrorCodeUserDisabled - Indicates the user's account is disabled.
+ </li>
+ <li>@c FIRAuthErrorCodeWrongPassword - Indicates the user attempted
+ sign in with an incorrect password.
+ </li>
+ <li>@c FIRAuthErrorCodeInvalidEmail - Indicates the email address is malformed.
+ </li>
+ </ul>
+
+ @remarks See @c FIRAuthErrors for a list of error codes that are common to all API methods.
+ */
+- (void)signInWithEmail:(NSString *)email
+ password:(NSString *)password
+ completion:(nullable FIRAuthResultCallback)completion;
+
+/** @fn signInWithCredential:completion:
+ @brief Convenience method for @c signInAndRetrieveDataWithCredential:completion: This method
+ doesn't return additional identity provider data.
+ */
+- (void)signInWithCredential:(FIRAuthCredential *)credential
+ completion:(nullable FIRAuthResultCallback)completion;
+
+/** @fn signInAndRetrieveDataWithCredential:completion:
+ @brief Asynchronously signs in to Firebase with the given 3rd-party credentials (e.g. a Facebook
+ login Access Token, a Google ID Token/Access Token pair, etc.) and returns additional
+ identity provider data.
+
+ @param credential The credential supplied by the IdP.
+ @param completion Optionally; a block which is invoked when the sign in flow finishes, or is
+ canceled. Invoked asynchronously on the main thread in the future.
+
+ @remarks Possible error codes:
+ <ul>
+ <li>@c FIRAuthErrorCodeInvalidCredential - Indicates the supplied credential is invalid.
+ This could happen if it has expired or it is malformed.
+ </li>
+ <li>@c FIRAuthErrorCodeOperationNotAllowed - Indicates that accounts
+ with the identity provider represented by the credential are not enabled.
+ Enable them in the Auth section of the Firebase console.
+ </li>
+ <li>@c FIRAuthErrorCodeAccountExistsWithDifferentCredential - Indicates the email asserted
+ by the credential (e.g. the email in a Facebook access token) is already in use by an
+ existing account, that cannot be authenticated with this sign-in method. Call
+ fetchProvidersForEmail for this user’s email and then prompt them to sign in with any of
+ the sign-in providers returned. This error will only be thrown if the "One account per
+ email address" setting is enabled in the Firebase console, under Auth settings.
+ </li>
+ <li>@c FIRAuthErrorCodeUserDisabled - Indicates the user's account is disabled.
+ </li>
+ <li>@c FIRAuthErrorCodeWrongPassword - Indicates the user attempted sign in with an
+ incorrect password, if credential is of the type EmailPasswordAuthCredential.
+ </li>
+ <li>@c FIRAuthErrorCodeInvalidEmail - Indicates the email address is malformed.
+ </li>
+ </ul>
+
+ @remarks See @c FIRAuthErrors for a list of error codes that are common to all API methods.
+ */
+- (void)signInAndRetrieveDataWithCredential:(FIRAuthCredential *)credential
+ completion:(nullable FIRAuthDataResultCallback)completion;
+
+/** @fn signInAnonymouslyWithCompletion:
+ @brief Asynchronously creates and becomes an anonymous user.
+ @param completion Optionally; a block which is invoked when the sign in finishes, or is
+ canceled. Invoked asynchronously on the main thread in the future.
+
+ @remarks If there is already an anonymous user signed in, that user will be returned instead.
+ If there is any other existing user signed in, that user will be signed out.
+
+ @remarks Possible error codes:
+ <ul>
+ <li>@c FIRAuthErrorCodeOperationNotAllowed - Indicates that anonymous accounts are
+ not enabled. Enable them in the Auth section of the Firebase console.
+ </li>
+ </ul>
+
+ @remarks See @c FIRAuthErrors for a list of error codes that are common to all API methods.
+ */
+- (void)signInAnonymouslyWithCompletion:(nullable FIRAuthResultCallback)completion;
+
+/** @fn signInWithCustomToken:completion:
+ @brief Asynchronously signs in to Firebase with the given Auth token.
+
+ @param token A self-signed custom auth token.
+ @param completion Optionally; a block which is invoked when the sign in finishes, or is
+ canceled. Invoked asynchronously on the main thread in the future.
+
+ @remarks Possible error codes:
+ <ul>
+ <li>@c FIRAuthErrorCodeInvalidCustomToken - Indicates a validation error with
+ the custom token.
+ </li>
+ <li>@c FIRAuthErrorCodeCustomTokenMismatch - Indicates the service account and the API key
+ belong to different projects.
+ </li>
+ </ul>
+
+ @remarks See @c FIRAuthErrors for a list of error codes that are common to all API methods.
+ */
+- (void)signInWithCustomToken:(NSString *)token
+ completion:(nullable FIRAuthResultCallback)completion;
+
+/** @fn createUserWithEmail:password:completion:
+ @brief Creates and, on success, signs in a user with the given email address and password.
+
+ @param email The user's email address.
+ @param password The user's desired password.
+ @param completion Optionally; a block which is invoked when the sign up flow finishes, or is
+ canceled. Invoked asynchronously on the main thread in the future.
+
+ @remarks Possible error codes:
+ <ul>
+ <li>@c FIRAuthErrorCodeInvalidEmail - Indicates the email address is malformed.
+ </li>
+ <li>@c FIRAuthErrorCodeEmailAlreadyInUse - Indicates the email used to attempt sign up
+ already exists. Call fetchProvidersForEmail to check which sign-in mechanisms the user
+ used, and prompt the user to sign in with one of those.
+ </li>
+ <li>@c FIRAuthErrorCodeOperationNotAllowed - Indicates that email and password accounts
+ are not enabled. Enable them in the Auth section of the Firebase console.
+ </li>
+ <li>@c FIRAuthErrorCodeWeakPassword - Indicates an attempt to set a password that is
+ considered too weak. The NSLocalizedFailureReasonErrorKey field in the NSError.userInfo
+ dictionary object will contain more detailed explanation that can be shown to the user.
+ </li>
+ </ul>
+
+ @remarks See @c FIRAuthErrors for a list of error codes that are common to all API methods.
+ */
+- (void)createUserWithEmail:(NSString *)email
+ password:(NSString *)password
+ completion:(nullable FIRAuthResultCallback)completion;
+
+/** @fn confirmPasswordResetWithCode:newPassword:completion:
+ @brief Resets the password given a code sent to the user outside of the app and a new password
+ for the user.
+
+ @param newPassword The new password.
+ @param completion Optionally; a block which is invoked when the request finishes. Invoked
+ asynchronously on the main thread in the future.
+
+ @remarks Possible error codes:
+ <ul>
+ <li>@c FIRAuthErrorCodeWeakPassword - Indicates an attempt to set a password that is
+ considered too weak.
+ </li>
+ <li>@c FIRAuthErrorCodeOperationNotAllowed - Indicates the administrator disabled sign
+ in with the specified identity provider.
+ </li>
+ <li>@c FIRAuthErrorCodeExpiredActionCode - Indicates the OOB code is expired.
+ </li>
+ <li>@c FIRAuthErrorCodeInvalidActionCode - Indicates the OOB code is invalid.
+ </li>
+ </ul>
+
+ @remarks See @c FIRAuthErrors for a list of error codes that are common to all API methods.
+ */
+- (void)confirmPasswordResetWithCode:(NSString *)code
+ newPassword:(NSString *)newPassword
+ completion:(FIRConfirmPasswordResetCallback)completion;
+
+/** @fn checkActionCode:completion:
+ @brief Checks the validity of an out of band code.
+
+ @param code The out of band code to check validity.
+ @param completion Optionally; a block which is invoked when the request finishes. Invoked
+ asynchronously on the main thread in the future.
+ */
+- (void)checkActionCode:(NSString *)code completion:(FIRCheckActionCodeCallBack)completion;
+
+/** @fn verifyPasswordResetCode:completion:
+ @brief Checks the validity of a verify password reset code.
+
+ @param code The password reset code to be verified.
+ @param completion Optionally; a block which is invoked when the request finishes. Invoked
+ asynchronously on the main thread in the future.
+ */
+- (void)verifyPasswordResetCode:(NSString *)code
+ completion:(FIRVerifyPasswordResetCodeCallback)completion;
+
+/** @fn applyActionCode:completion:
+ @brief Applies out of band code.
+
+ @param code The out of band code to be applied.
+ @param completion Optionally; a block which is invoked when the request finishes. Invoked
+ asynchronously on the main thread in the future.
+
+ @remarks This method will not work for out of band codes which require an additional parameter,
+ such as password reset code.
+ */
+- (void)applyActionCode:(NSString *)code
+ completion:(FIRApplyActionCodeCallback)completion;
+
+/** @fn sendPasswordResetWithEmail:completion:
+ @brief Initiates a password reset for the given email address.
+
+ @param email The email address of the user.
+ @param completion Optionally; a block which is invoked when the request finishes. Invoked
+ asynchronously on the main thread in the future.
+
+ @remarks Possible error codes:
+ <ul>
+ <li>@c FIRAuthErrorCodeInvalidRecipientEmail - Indicates an invalid recipient email was
+ sent in the request.
+ </li>
+ <li>@c FIRAuthErrorCodeInvalidSender - Indicates an invalid sender email is set in
+ the console for this action.
+ </li>
+ <li>@c FIRAuthErrorCodeInvalidMessagePayload - Indicates an invalid email template for
+ sending update email.
+ </li>
+ </ul>
+ */
+- (void)sendPasswordResetWithEmail:(NSString *)email
+ completion:(nullable FIRSendPasswordResetCallback)completion;
+
+/** @fn signOut:
+ @brief Signs out the current user.
+
+ @param error Optionally; if an error occurs, upon return contains an NSError object that
+ describes the problem; is nil otherwise.
+ @return @YES when the sign out request was successful. @NO otherwise.
+
+ @remarks Possible error codes:
+ <ul>
+ <li>@c FIRAuthErrorCodeKeychainError - Indicates an error occurred when accessing the
+ keychain. The @c NSLocalizedFailureReasonErrorKey field in the @c NSError.userInfo
+ dictionary will contain more information about the error encountered.
+ </li>
+ </ul>
+
+ */
+- (BOOL)signOut:(NSError *_Nullable *_Nullable)error;
+
+/** @fn addAuthStateDidChangeListener:
+ @brief Registers a block as an "auth state did change" listener. To be invoked when:
+
+ + The block is registered as a listener,
+ + A user with a different UID from the current user has signed in, or
+ + The current user has signed out.
+
+ @param listener The block to be invoked. The block is always invoked asynchronously on the main
+ thread, even for it's initial invocation after having been added as a listener.
+
+ @remarks The block is invoked immediately after adding it according to it's standard invocation
+ semantics, asynchronously on the main thread. Users should pay special attention to
+ making sure the block does not inadvertently retain objects which should not be retained by
+ the long-lived block. The block itself will be retained by @c FIRAuth until it is
+ unregistered or until the @c FIRAuth instance is otherwise deallocated.
+
+ @return A handle useful for manually unregistering the block as a listener.
+ */
+- (FIRAuthStateDidChangeListenerHandle)addAuthStateDidChangeListener:
+ (FIRAuthStateDidChangeListenerBlock)listener;
+
+/** @fn removeAuthStateDidChangeListener:
+ @brief Unregisters a block as an "auth state did change" listener.
+
+ @param listenerHandle The handle for the listener.
+ */
+- (void)removeAuthStateDidChangeListener:(FIRAuthStateDidChangeListenerHandle)listenerHandle;
+
+/** @fn addIDTokenDidChangeListener:
+ @brief Registers a block as an "ID token did change" listener. To be invoked when:
+
+ + The block is registered as a listener,
+ + A user with a different UID from the current user has signed in,
+ + The ID token of the current user has been refreshed, or
+ + The current user has signed out.
+
+ @param listener The block to be invoked. The block is always invoked asynchronously on the main
+ thread, even for it's initial invocation after having been added as a listener.
+
+ @remarks The block is invoked immediately after adding it according to it's standard invocation
+ semantics, asynchronously on the main thread. Users should pay special attention to
+ making sure the block does not inadvertently retain objects which should not be retained by
+ the long-lived block. The block itself will be retained by @c FIRAuth until it is
+ unregistered or until the @c FIRAuth instance is otherwise deallocated.
+
+ @return A handle useful for manually unregistering the block as a listener.
+ */
+- (FIRIDTokenDidChangeListenerHandle)addIDTokenDidChangeListener:
+ (FIRIDTokenDidChangeListenerBlock)listener;
+
+/** @fn removeIDTokenDidChangeListener:
+ @brief Unregisters a block as an "ID token did change" listener.
+
+ @param listenerHandle The handle for the listener.
+ */
+- (void)removeIDTokenDidChangeListener:(FIRIDTokenDidChangeListenerHandle)listenerHandle;
+
+/** @fn setAPNSToken:type:
+ @brief Sets the APNs token along with its type.
+ @remarks If swizzling is disabled, the APNs Token must be set for phone number auth to work,
+ by either setting calling this method or by setting the @c APNSToken property.
+ */
+- (void)setAPNSToken:(NSData *)token type:(FIRAuthAPNSTokenType)type;
+
+/** @fn canHandleNotification:
+ @brief Whether the specific remote notification is handled by @c FIRAuth .
+ @param userInfo A dictionary that contains information related to the
+ notification in question.
+ @return Whether or the notification is handled. @c YES means the notification is for @c FIRAuth
+ so the caller should ignore the notification from further processing, and @c NO means the
+ the notification is for the app (or another libaray) so the caller should continue handling
+ this notification as usual.
+ @remarks If swizzling is disabled, related remote notifications must be forwarded to this method
+ for phone number auth to work.
+ */
+- (BOOL)canHandleNotification:(NSDictionary *)userInfo;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/FIRAuth.m b/Firebase/Auth/Source/FIRAuth.m
new file mode 100644
index 0000000..d1beae6
--- /dev/null
+++ b/Firebase/Auth/Source/FIRAuth.m
@@ -0,0 +1,1252 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "Private/FIRAuth_Internal.h"
+
+#import "FIRAppAssociationRegistration.h"
+#import "FIRAppInternal.h"
+#import "FIROptions.h"
+#import "FIRLogger.h"
+#import "AuthProviders/EmailPassword/FIREmailPasswordAuthCredential.h"
+#import "AuthProviders/Phone/FIRPhoneAuthCredential_Internal.h"
+#import "Private/FIRAdditionalUserInfo_Internal.h"
+#import "Private/FIRAuthAPNSToken.h"
+#import "Private/FIRAuthAPNSTokenManager.h"
+#import "Private/FIRAuthAppCredentialManager.h"
+#import "Private/FIRAuthAppDelegateProxy.h"
+#import "Private/FIRAuthCredential_Internal.h"
+#import "Private/FIRAuthDataResult_Internal.h"
+#import "Private/FIRAuthDispatcher.h"
+#import "Private/FIRAuthErrorUtils.h"
+#import "FIRAuthExceptionUtils.h"
+#import "Private/FIRAuthGlobalWorkQueue.h"
+#import "Private/FIRAuthKeychain.h"
+#import "Private/FIRAuthNotificationManager.h"
+#import "Private/FIRUser_Internal.h"
+#import "FirebaseAuth.h"
+#import "FIRAuthBackend.h"
+#import "FIRCreateAuthURIRequest.h"
+#import "FIRCreateAuthURIResponse.h"
+#import "FIRGetOOBConfirmationCodeRequest.h"
+#import "FIRGetOOBConfirmationCodeResponse.h"
+#import "FIRResetPasswordRequest.h"
+#import "FIRResetPasswordResponse.h"
+#import "FIRSendVerificationCodeRequest.h"
+#import "FIRSendVerificationCodeResponse.h"
+#import "FIRSetAccountInfoRequest.h"
+#import "FIRSetAccountInfoResponse.h"
+#import "FIRSignUpNewUserRequest.h"
+#import "FIRSignUpNewUserResponse.h"
+#import "FIRVerifyAssertionRequest.h"
+#import "FIRVerifyAssertionResponse.h"
+#import "FIRVerifyCustomTokenRequest.h"
+#import "FIRVerifyCustomTokenResponse.h"
+#import "FIRVerifyPasswordRequest.h"
+#import "FIRVerifyPasswordResponse.h"
+#import "FIRVerifyPhoneNumberRequest.h"
+#import "FIRVerifyPhoneNumberResponse.h"
+
+#pragma mark - Constants
+
+NSString *const FIRAuthStateDidChangeInternalNotification =
+ @"FIRAuthStateDidChangeInternalNotification";
+NSString *const FIRAuthStateDidChangeInternalNotificationTokenKey =
+ @"FIRAuthStateDidChangeInternalNotificationTokenKey";
+
+#if defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0
+const NSNotificationName FIRAuthStateDidChangeNotification = @"FIRAuthStateDidChangeNotification";
+#else
+NSString *const FIRAuthStateDidChangeNotification = @"FIRAuthStateDidChangeNotification";
+#endif // defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0
+
+/** @var kMaxWaitTimeForBackoff
+ @brief The maximum wait time before attempting to retry auto refreshing tokens after a failed
+ attempt.
+ @remarks This is the upper limit (in seconds) of the exponential backoff used for retrying
+ token refresh.
+ */
+static NSTimeInterval kMaxWaitTimeForBackoff = 16 * 60;
+
+/** @var kTokenRefreshHeadStart
+ @brief The amount of time before the token expires that proactive refresh should be attempted.
+ */
+NSTimeInterval kTokenRefreshHeadStart = 5 * 60;
+
+/** @var kUserKey
+ @brief Key of user stored in the keychain. Prefixed with a Firebase app name.
+ */
+static NSString *const kUserKey = @"%@_firebase_user";
+
+/** @var kMissingEmailInvalidParameterExceptionReason
+ @brief The key of missing email key @c invalidParameterException.
+ */
+static NSString *const kEmailInvalidParameterReason = @"The email used to initiate user password "
+ "cannot be nil";
+
+static NSString *const kPasswordResetRequestType = @"PASSWORD_RESET";
+
+static NSString *const kVerifyEmailRequestType = @"VERIFY_EMAIL";
+
+/** @var kMissingPasswordReason
+ @brief The reason why the @c FIRAuthErrorCodeWeakPassword error is thrown.
+ @remarks This error message will be localized in the future.
+ */
+static NSString *const kMissingPasswordReason = @"Missing Password";
+
+/** @var gKeychainServiceNameForAppName
+ @brief A map from Firebase app name to keychain service names.
+ @remarks This map is needed for looking up the keychain service name after the FIRApp instance
+ is deleted, to remove the associated keychain item. Accessing should occur within a
+ @syncronized([FIRAuth class]) context.
+ */
+static NSMutableDictionary *gKeychainServiceNameForAppName;
+
+#pragma mark - FIRActionCodeInfo
+
+@implementation FIRActionCodeInfo {
+ /** @var _email
+ @brief The email address to which the code was sent. The new email address in the case of
+ FIRActionCodeOperationRecoverEmail.
+ */
+ NSString *_email;
+
+ /** @var _fromEmail
+ @brief The current email address in the case of FIRActionCodeOperationRecoverEmail.
+ */
+ NSString *_fromEmail;
+}
+
+- (NSString *)dataForKey:(FIRActionDataKey)key{
+ switch (key) {
+ case FIRActionCodeEmailKey:
+ return _email;
+ case FIRActionCodeFromEmailKey:
+ return _fromEmail;
+ }
+}
+
+- (instancetype)initWithOperation:(FIRActionCodeOperation)operation
+ email:(NSString *)email
+ newEmail:(nullable NSString *)newEmail {
+ self = [super init];
+ if (self) {
+ _operation = operation;
+ if (newEmail) {
+ _email = [newEmail copy];
+ _fromEmail = [email copy];
+ } else {
+ _email = [email copy];
+ }
+ }
+ return self;
+}
+
+/** @fn actionCodeOperationForRequestType:
+ @brief Returns the corresponding operation type per provided request type string.
+ @param requestType Request type returned in in the server response.
+ @return The corresponding FIRActionCodeOperation for the supplied request type.
+ */
++ (FIRActionCodeOperation)actionCodeOperationForRequestType:(NSString *)requestType {
+ if ([requestType isEqualToString:kPasswordResetRequestType]) {
+ return FIRActionCodeOperationPasswordReset;
+ }
+ if ([requestType isEqualToString:kVerifyEmailRequestType]) {
+ return FIRActionCodeOperationVerifyEmail;
+ }
+ return FIRActionCodeOperationUnknown;
+}
+
+@end
+
+#pragma mark - FIRAuth
+
+@interface FIRAuth () <FIRAuthAppDelegateHandler>
+
+/** @property firebaseAppId
+ @brief The Firebase app ID.
+ */
+@property(nonatomic, copy, readonly) NSString *firebaseAppId;
+
+/** @fn initWithApp:
+ @brief Creates a @c FIRAuth instance associated with the provided @c FIRApp instance.
+ @param app The application to associate the auth instance with.
+ */
+- (instancetype)initWithApp:(FIRApp *)app;
+
+@end
+
+@implementation FIRAuth {
+ /** @var _firebaseAppName
+ @brief The Firebase app name.
+ */
+ NSString *_firebaseAppName;
+
+ /** @var _listenerHandles
+ @brief Handles returned from @c NSNotificationCenter for blocks which are "auth state did
+ change" notification listeners.
+ @remarks Mutations should occur within a @syncronized(self) context.
+ */
+ NSMutableArray<FIRAuthStateDidChangeListenerHandle> *_listenerHandles;
+
+ /** @var _keychain
+ @brief The keychain service.
+ */
+ FIRAuthKeychain *_keychain;
+
+ /** @var _autoRefreshTokens
+ @brief This flag denotes whether or not tokens should be automatically refreshed.
+ @remarks Will only be set to @YES if the another Firebase service is included (additionally to
+ Firebase Auth).
+ */
+ BOOL _autoRefreshTokens;
+
+ /** @var _autoRefreshScheduled
+ @brief Whether or not token auto-refresh is currently scheduled.
+ */
+ BOOL _autoRefreshScheduled;
+
+ /** @var _isAppInBackground
+ @brief A flag that is set to YES if the app is put in the background and no when the app is
+ returned to the foreground.
+ */
+ BOOL _isAppInBackground;
+
+ /** @var _applicationDidBecomeActiveObserver
+ @brief An opaque object to act as the observer for UIApplicationDidBecomeActiveNotification.
+ */
+ id<NSObject> _applicationDidBecomeActiveObserver;
+
+ /** @var _applicationDidBecomeActiveObserver
+ @brief An opaque object to act as the observer for
+ UIApplicationDidEnterBackgroundNotification.
+ */
+ id<NSObject> _applicationDidEnterBackgroundObserver;
+}
+
++ (void)load {
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ gKeychainServiceNameForAppName = [[NSMutableDictionary alloc] init];
+
+ NSNotificationCenter *defaultCenter = [NSNotificationCenter defaultCenter];
+
+ // Ensures the @c FIRAuth instance for a given app gets loaded as soon as the app is ready.
+ [defaultCenter addObserverForName:kFIRAppReadyToConfigureSDKNotification
+ object:[FIRApp class]
+ queue:nil
+ usingBlock:^(NSNotification *notification) {
+ [FIRAuth authWithApp:[FIRApp appNamed:notification.userInfo[kFIRAppNameKey]]];
+ }];
+ // Ensures the saved user is cleared when the app is deleted.
+ [defaultCenter addObserverForName:kFIRAppDeleteNotification
+ object:[FIRApp class]
+ queue:nil
+ usingBlock:^(NSNotification *notification) {
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^{
+ // This doesn't stop any request already issued, see b/27704535 .
+ NSString *appName = notification.userInfo[kFIRAppNameKey];
+ NSString *keychainServiceName = [FIRAuth keychainServiceNameForAppName:appName];
+ if (keychainServiceName) {
+ [self deleteKeychainServiceNameForAppName:appName];
+ FIRAuthKeychain *keychain = [[FIRAuthKeychain alloc] initWithService:keychainServiceName];
+ NSString *userKey = [NSString stringWithFormat:kUserKey, appName];
+ [keychain removeDataForKey:userKey error:NULL];
+ }
+ dispatch_async(dispatch_get_main_queue(), ^{
+ [[NSNotificationCenter defaultCenter]
+ postNotificationName:FIRAuthStateDidChangeNotification
+ object:nil];
+ });
+ });
+ }];
+ });
+}
+
++ (FIRAuth *)auth {
+ FIRApp *defaultApp = [FIRApp defaultApp];
+ if (!defaultApp) {
+ [NSException raise:NSInternalInconsistencyException
+ format:@"The default FIRApp instance must be configured before the default FIRAuth"
+ @"instance can be initialized. One way to ensure that is to call "
+ @"`[FIRApp configure];` is called in "
+ @"`application:didFinishLaunchingWithOptions:`."];
+ }
+ return [self authWithApp:defaultApp];
+}
+
++ (FIRAuth *)authWithApp:(FIRApp *)app {
+ return [FIRAppAssociationRegistration registeredObjectWithHost:app
+ key:NSStringFromClass(self)
+ creationBlock:^FIRAuth *_Nullable() {
+ return [[FIRAuth alloc] initWithApp:app];
+ }];
+}
+
+- (instancetype)initWithApp:(FIRApp *)app {
+ [FIRAuth setKeychainServiceNameForApp:app];
+ self = [self initWithAPIKey:app.options.APIKey appName:app.name];
+ if (self) {
+ _app = app;
+ __weak FIRAuth *weakSelf = self;
+ app.getTokenImplementation = ^(BOOL forceRefresh, FIRTokenCallback callback) {
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^{
+ FIRAuth *strongSelf = weakSelf;
+ if (strongSelf && !strongSelf->_autoRefreshTokens) {
+ FIRLogInfo(kFIRLoggerAuth, @"I-AUT000002", @"Token auto-refresh enabled.");
+ strongSelf->_autoRefreshTokens = YES;
+ strongSelf->_applicationDidBecomeActiveObserver = [[NSNotificationCenter defaultCenter]
+ addObserverForName:UIApplicationDidBecomeActiveNotification
+ object:nil
+ queue:nil
+ usingBlock:^(NSNotification *notification) {
+ FIRAuth *strongSelf = weakSelf;
+ if (strongSelf) {
+ strongSelf->_isAppInBackground = NO;
+ if (!strongSelf->_autoRefreshScheduled) {
+ [weakSelf scheduleAutoTokenRefresh];
+ }
+ }
+ }];
+ strongSelf->_applicationDidEnterBackgroundObserver = [[NSNotificationCenter defaultCenter]
+ addObserverForName:UIApplicationDidEnterBackgroundNotification
+ object:nil
+ queue:nil
+ usingBlock:^(NSNotification *notification) {
+ FIRAuth *strongSelf = weakSelf;
+ if (strongSelf) {
+ strongSelf->_isAppInBackground = YES;
+ }
+ }];
+ }
+ if (!strongSelf.currentUser) {
+ dispatch_async(dispatch_get_main_queue(), ^{
+ callback(nil, nil);
+ });
+ return;
+ }
+ [strongSelf.currentUser internalGetTokenForcingRefresh:forceRefresh
+ callback:^(NSString *_Nullable token,
+ NSError *_Nullable error) {
+ dispatch_async(dispatch_get_main_queue(), ^{
+ callback(token, error);
+ });
+ }];
+ });
+ };
+ app.getUIDImplementation = ^NSString *_Nullable() {
+ __block NSString *uid;
+ dispatch_sync(FIRAuthGlobalWorkQueue(), ^{
+ uid = [weakSelf getUID];
+ });
+ return uid;
+ };
+ }
+ return self;
+}
+
+- (instancetype)initWithAPIKey:(NSString *)APIKey appName:(NSString *)appName {
+ self = [super init];
+ if (self) {
+ _listenerHandles = [NSMutableArray array];
+ _APIKey = [APIKey copy];
+ _firebaseAppName = [appName copy];
+ NSString *keychainServiceName = [FIRAuth keychainServiceNameForAppName:appName];
+ if (keychainServiceName) {
+ _keychain = [[FIRAuthKeychain alloc] initWithService:keychainServiceName];
+ }
+ // Load current user from keychain.
+ FIRUser *user;
+ NSError *error;
+ if ([self getUser:&user error:&error]) {
+ [self updateCurrentUser:user byForce:NO savingToDisk:NO error:&error];
+ } else {
+ FIRLogError(kFIRLoggerAuth, @"I-AUT000001",
+ @"Error loading saved user when starting up: %@", error);
+ }
+ // Initialize for phone number auth.
+ _tokenManager =
+ [[FIRAuthAPNSTokenManager alloc] initWithApplication:[UIApplication sharedApplication]];
+ _appCredentialManager = [[FIRAuthAppCredentialManager alloc] initWithKeychain:_keychain];
+ _notificationManager =
+ [[FIRAuthNotificationManager alloc] initWithApplication:[UIApplication sharedApplication]
+ appCredentialManager:_appCredentialManager];
+ [[FIRAuthAppDelegateProxy sharedInstance] addHandler:self];
+ }
+ return self;
+}
+
+- (void)dealloc {
+ @synchronized (self) {
+ NSNotificationCenter *defaultCenter = [NSNotificationCenter defaultCenter];
+ while (_listenerHandles.count != 0) {
+ FIRAuthStateDidChangeListenerHandle handleToRemove = _listenerHandles.lastObject;
+ [defaultCenter removeObserver:handleToRemove];
+ [_listenerHandles removeLastObject];
+ }
+ [defaultCenter removeObserver:_applicationDidBecomeActiveObserver
+ name:UIApplicationDidBecomeActiveNotification
+ object:nil];
+ [defaultCenter removeObserver:_applicationDidEnterBackgroundObserver
+ name:UIApplicationDidEnterBackgroundNotification
+ object:nil];
+ }
+}
+
+#pragma mark - Public API
+
+- (void)fetchProvidersForEmail:(NSString *)email
+ completion:(FIRProviderQueryCallback)completion {
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^{
+ FIRCreateAuthURIRequest *request =
+ [[FIRCreateAuthURIRequest alloc] initWithIdentifier:email
+ continueURI:@"http://www.google.com/"
+ APIKey:_APIKey];
+ [FIRAuthBackend createAuthURI:request callback:^(FIRCreateAuthURIResponse *_Nullable response,
+ NSError *_Nullable error) {
+ if (completion) {
+ dispatch_async(dispatch_get_main_queue(), ^{
+ completion(response.allProviders, error);
+ });
+ }
+ }];
+ });
+}
+
+- (void)signInWithEmail:(NSString *)email
+ password:(NSString *)password
+ completion:(FIRAuthResultCallback)completion {
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^{
+ [self signInWithEmail:email
+ password:password
+ callback:[self signInFlowAuthResultCallbackByDecoratingCallback:completion]];
+ });
+}
+
+/** @fn signInWithEmail:password:callback:
+ @brief Signs in using an email address and password.
+ @param email The user's email address.
+ @param password The user's password.
+ @param callback A block which is invoked when the sign in finishes (or is cancelled.) Invoked
+ asynchronously on the global auth work queue in the future.
+ @remarks This is the internal counterpart of this method, which uses a callback that does not
+ update the current user.
+ */
+- (void)signInWithEmail:(NSString *)email
+ password:(NSString *)password
+ callback:(FIRAuthResultCallback)callback {
+ FIRVerifyPasswordRequest *request =
+ [[FIRVerifyPasswordRequest alloc] initWithEmail:email password:password APIKey:_APIKey];
+
+ if (![request.password length]) {
+ callback(nil, [FIRAuthErrorUtils wrongPasswordErrorWithMessage:nil]);
+ return;
+ }
+ [FIRAuthBackend verifyPassword:request
+ callback:^(FIRVerifyPasswordResponse *_Nullable response,
+ NSError *_Nullable error) {
+ if (error) {
+ callback(nil, error);
+ return;
+ }
+ [self completeSignInWithAccessToken:response.IDToken
+ accessTokenExpirationDate:response.approximateExpirationDate
+ refreshToken:response.refreshToken
+ anonymous:NO
+ callback:callback];
+ }];
+}
+
+- (void)signInWithCredential:(FIRAuthCredential *)credential
+ completion:(FIRAuthResultCallback)completion {
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^{
+ FIRAuthResultCallback callback =
+ [self signInFlowAuthResultCallbackByDecoratingCallback:completion];
+ [self internalSignInWithCredential:credential callback:callback];
+ });
+}
+
+- (void)signInAndRetrieveDataWithCredential:(FIRAuthCredential *)credential
+ completion:(nullable FIRAuthDataResultCallback)completion {
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^{
+ FIRAuthDataResultCallback callback =
+ [self signInFlowAuthDataResultCallbackByDecoratingCallback:completion];
+ [self internalSignInAndRetrieveDataWithCredential:credential
+ isReauthentication:NO
+ callback:callback];
+ });
+}
+
+- (void)internalSignInWithCredential:(FIRAuthCredential *)credential
+ callback:(FIRAuthResultCallback)callback {
+ [self internalSignInAndRetrieveDataWithCredential:credential
+ isReauthentication:NO
+ callback:^(FIRAuthDataResult *_Nullable authResult,
+ NSError *_Nullable error) {
+ callback(authResult.user, error);
+ }];
+}
+
+- (void)internalSignInAndRetrieveDataWithCredential:(FIRAuthCredential *)credential
+ isReauthentication:(BOOL)isReauthentication
+ callback:(nullable FIRAuthDataResultCallback)callback {
+ if ([credential isKindOfClass:[FIREmailPasswordAuthCredential class]]) {
+ // Special case for email/password credentials:
+ FIREmailPasswordAuthCredential *emailPasswordCredential =
+ (FIREmailPasswordAuthCredential *)credential;
+ [self signInWithEmail:emailPasswordCredential.email
+ password:emailPasswordCredential.password
+ callback:^(FIRUser *_Nullable user, NSError *_Nullable error) {
+ if (callback) {
+ FIRAuthDataResult *result = user ?
+ [[FIRAuthDataResult alloc] initWithUser:user additionalUserInfo:nil] : nil;
+ callback(result, error);
+ }
+ }];
+ return;
+ }
+
+ if ([credential isKindOfClass:[FIRPhoneAuthCredential class]]) {
+ // Special case for phone auth credential
+ FIRPhoneAuthCredential *phoneCredential = (FIRPhoneAuthCredential *)credential;
+ [self signInWithPhoneCredential:phoneCredential callback:^(FIRUser *_Nullable user,
+ NSError *_Nullable error) {
+ if (callback) {
+ FIRAuthDataResult *result = user ?
+ [[FIRAuthDataResult alloc] initWithUser:user additionalUserInfo:nil] : nil;
+ callback(result, error);
+ }
+ }];
+ return;
+ }
+
+ FIRVerifyAssertionRequest *request =
+ [[FIRVerifyAssertionRequest alloc] initWithAPIKey:_APIKey providerID:credential.provider];
+ request.autoCreate = !isReauthentication;
+ [credential prepareVerifyAssertionRequest:request];
+ [FIRAuthBackend verifyAssertion:request
+ callback:^(FIRVerifyAssertionResponse *response, NSError *error) {
+ if (error) {
+ if (callback) {
+ callback(nil, error);
+ }
+ return;
+ }
+
+ if (response.needConfirmation) {
+ if (callback) {
+ NSString *email = response.email;
+ callback(nil, [FIRAuthErrorUtils accountExistsWithDifferentCredentialErrorWithEmail:email]);
+ }
+ return;
+ }
+
+ if (!response.providerID.length) {
+ if (callback) {
+ callback(nil, [FIRAuthErrorUtils unexpectedResponseWithDeserializedResponse:response]);
+ }
+ return;
+ }
+ [self completeSignInWithAccessToken:response.IDToken
+ accessTokenExpirationDate:response.approximateExpirationDate
+ refreshToken:response.refreshToken
+ anonymous:NO
+ callback:^(FIRUser *_Nullable user, NSError *_Nullable error) {
+ if (callback) {
+ FIRAdditionalUserInfo *additionalUserInfo =
+ [FIRAdditionalUserInfo userInfoWithVerifyAssertionResponse:response];
+ FIRAuthDataResult *result = user ?
+ [[FIRAuthDataResult alloc] initWithUser:user
+ additionalUserInfo:additionalUserInfo] : nil;
+ callback(result, error);
+ }
+ }];
+ }];
+}
+
+- (void)signInWithCredential:(FIRAuthCredential *)credential
+ callback:(FIRAuthResultCallback)callback {
+ [self signInAndRetrieveDataWithCredential:credential
+ completion:^(FIRAuthDataResult *_Nullable authResult,
+ NSError *_Nullable error) {
+ callback(authResult.user, error);
+ }];
+}
+
+- (void)signInAnonymouslyWithCompletion:(FIRAuthResultCallback)completion {
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^{
+ FIRAuthResultCallback decoratedCallback =
+ [self signInFlowAuthResultCallbackByDecoratingCallback:completion];
+ if (_currentUser.anonymous) {
+ decoratedCallback(_currentUser, nil);
+ return;
+ }
+ FIRSignUpNewUserRequest *request = [[FIRSignUpNewUserRequest alloc] initWithAPIKey:_APIKey];
+ [FIRAuthBackend signUpNewUser:request
+ callback:^(FIRSignUpNewUserResponse *_Nullable response,
+ NSError *_Nullable error) {
+ if (error) {
+ decoratedCallback(nil, error);
+ return;
+ }
+ [self completeSignInWithAccessToken:response.IDToken
+ accessTokenExpirationDate:response.approximateExpirationDate
+ refreshToken:response.refreshToken
+ anonymous:YES
+ callback:decoratedCallback];
+ }];
+ });
+}
+
+- (void)signInWithCustomToken:(NSString *)token
+ completion:(nullable FIRAuthResultCallback)completion {
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^{
+ FIRAuthResultCallback decoratedCallback =
+ [self signInFlowAuthResultCallbackByDecoratingCallback:completion];
+ FIRVerifyCustomTokenRequest *request =
+ [[FIRVerifyCustomTokenRequest alloc] initWithToken:token APIKey:_APIKey];
+ [FIRAuthBackend verifyCustomToken:request
+ callback:^(FIRVerifyCustomTokenResponse *_Nullable response,
+ NSError *_Nullable error) {
+ if (error) {
+ decoratedCallback(nil, error);
+ return;
+ }
+ [self completeSignInWithAccessToken:response.IDToken
+ accessTokenExpirationDate:response.approximateExpirationDate
+ refreshToken:response.refreshToken
+ anonymous:NO
+ callback:decoratedCallback];
+ }];
+ });
+}
+
+- (void)createUserWithEmail:(NSString *)email
+ password:(NSString *)password
+ completion:(nullable FIRAuthResultCallback)completion {
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^{
+ FIRAuthResultCallback decoratedCallback =
+ [self signInFlowAuthResultCallbackByDecoratingCallback:completion];
+ FIRSignUpNewUserRequest *request = [[FIRSignUpNewUserRequest alloc] initWithAPIKey:_APIKey
+ email:email
+ password:password
+ displayName:nil];
+ if (![request.password length]) {
+ decoratedCallback(nil, [FIRAuthErrorUtils
+ weakPasswordErrorWithServerResponseReason:kMissingPasswordReason]);
+ return;
+ }
+ [FIRAuthBackend signUpNewUser:request
+ callback:^(FIRSignUpNewUserResponse *_Nullable response,
+ NSError *_Nullable error) {
+ if (error) {
+ decoratedCallback(nil, error);
+ return;
+ }
+ [self completeSignInWithAccessToken:response.IDToken
+ accessTokenExpirationDate:response.approximateExpirationDate
+ refreshToken:response.refreshToken
+ anonymous:NO
+ callback:decoratedCallback];
+ }];
+ });
+}
+
+- (void)confirmPasswordResetWithCode:(NSString *)code
+ newPassword:(NSString *)newPassword
+ completion:(FIRConfirmPasswordResetCallback)completion {
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^{
+ FIRResetPasswordRequest *request =
+ [[FIRResetPasswordRequest alloc] initWithAPIKey:_APIKey
+ oobCode:code
+ newPassword:newPassword];
+ [FIRAuthBackend resetPassword:request callback:^(FIRResetPasswordResponse *_Nullable response,
+ NSError *_Nullable error) {
+ if (completion) {
+ dispatch_async(dispatch_get_main_queue(), ^{
+ if (error) {
+ completion(error);
+ return;
+ }
+ completion(nil);
+ });
+ }
+ }];
+ });
+}
+
+- (void)checkActionCode:(NSString *)code completion:(FIRCheckActionCodeCallBack)completion {
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^ {
+ FIRResetPasswordRequest *request =
+ [[FIRResetPasswordRequest alloc] initWithAPIKey:_APIKey
+ oobCode:code
+ newPassword:nil];
+ [FIRAuthBackend resetPassword:request callback:^(FIRResetPasswordResponse *_Nullable response,
+ NSError *_Nullable error) {
+ if (completion) {
+ if (error) {
+ dispatch_async(dispatch_get_main_queue(), ^{
+ completion(nil, error);
+ });
+ return;
+ }
+ FIRActionCodeOperation operation =
+ [FIRActionCodeInfo actionCodeOperationForRequestType:response.requestType];
+ FIRActionCodeInfo *actionCodeInfo =
+ [[FIRActionCodeInfo alloc] initWithOperation:operation
+ email:response.email
+ newEmail:response.verifiedEmail];
+ dispatch_async(dispatch_get_main_queue(), ^{
+ completion(actionCodeInfo, nil);
+ });
+ }
+ }];
+ });
+}
+
+- (void)verifyPasswordResetCode:(NSString *)code
+ completion:(FIRVerifyPasswordResetCodeCallback)completion {
+ [self checkActionCode:code completion:^(FIRActionCodeInfo *_Nullable info,
+ NSError *_Nullable error) {
+ if (completion) {
+ if (error) {
+ completion(nil, error);
+ return;
+ }
+ completion([info dataForKey:FIRActionCodeEmailKey], nil);
+ }
+ }];
+}
+
+- (void)applyActionCode:(NSString *)code completion:(FIRApplyActionCodeCallback)completion {
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^ {
+ FIRSetAccountInfoRequest *request = [[FIRSetAccountInfoRequest alloc]initWithAPIKey:_APIKey];
+ request.OOBCode = code;
+ [FIRAuthBackend setAccountInfo:request callback:^(FIRSetAccountInfoResponse *_Nullable response,
+ NSError *_Nullable error) {
+ if (completion) {
+ dispatch_async(dispatch_get_main_queue(), ^{
+ completion(error);
+ });
+ }
+ }];
+ });
+}
+
+- (void)sendPasswordResetWithEmail:(NSString *)email
+ completion:(nullable FIRSendPasswordResetCallback)completion {
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^{
+ if (!email) {
+ [FIRAuthExceptionUtils raiseInvalidParameterExceptionWithReason:kEmailInvalidParameterReason];
+ }
+ FIRGetOOBConfirmationCodeRequest *request =
+ [FIRGetOOBConfirmationCodeRequest passwordResetRequestWithEmail:email APIKey:_APIKey];
+ [FIRAuthBackend getOOBConfirmationCode:request
+ callback:^(FIRGetOOBConfirmationCodeResponse *_Nullable response,
+ NSError *_Nullable error) {
+ if (completion) {
+ dispatch_async(dispatch_get_main_queue(), ^{
+ completion(error);
+ });
+ }
+ }];
+ });
+}
+
+- (BOOL)signOut:(NSError *_Nullable *_Nullable)error {
+ __block BOOL result = YES;
+ dispatch_sync(FIRAuthGlobalWorkQueue(), ^{
+ if (!_currentUser) {
+ return;
+ }
+ result = [self updateCurrentUser:nil byForce:NO savingToDisk:YES error:error];
+ });
+ return result;
+}
+
+- (BOOL)signOutByForceWithUserID:(NSString *)userID error:(NSError *_Nullable *_Nullable)error {
+ if (_currentUser.uid != userID) {
+ return YES;
+ }
+ return [self updateCurrentUser:nil byForce:YES savingToDisk:YES error:error];
+}
+
+- (FIRAuthStateDidChangeListenerHandle)addAuthStateDidChangeListener:
+ (FIRAuthStateDidChangeListenerBlock)listener {
+ __block BOOL firstInvocation = YES;
+ __block NSString *previousUserID;
+ return [self addIDTokenDidChangeListener:^(FIRAuth *_Nonnull auth, FIRUser *_Nullable user) {
+ BOOL shouldCallListener = firstInvocation ||
+ !(previousUserID == user.uid || [previousUserID isEqualToString:user.uid]);
+ firstInvocation = NO;
+ previousUserID = [user.uid copy];
+ if (shouldCallListener) {
+ listener(auth, user);
+ }
+ }];
+}
+
+- (void)removeAuthStateDidChangeListener:(FIRAuthStateDidChangeListenerHandle)listenerHandle {
+ [self removeIDTokenDidChangeListener:listenerHandle];
+}
+
+- (FIRIDTokenDidChangeListenerHandle)addIDTokenDidChangeListener:
+ (FIRIDTokenDidChangeListenerBlock)listener {
+ if (!listener) {
+ [NSException raise:NSInvalidArgumentException format:@"listener must not be nil."];
+ return nil;
+ }
+ FIRAuthStateDidChangeListenerHandle handle;
+ NSNotificationCenter *notifications = [NSNotificationCenter defaultCenter];
+ handle = [notifications addObserverForName:FIRAuthStateDidChangeNotification
+ object:self
+ queue:[NSOperationQueue mainQueue]
+ usingBlock:^(NSNotification *_Nonnull notification) {
+ FIRAuth *auth = notification.object;
+ listener(auth, auth.currentUser);
+ }];
+ @synchronized (self) {
+ [_listenerHandles addObject:handle];
+ }
+ dispatch_async(dispatch_get_main_queue(), ^{
+ listener(self, self.currentUser);
+ });
+ return handle;
+}
+
+- (void)removeIDTokenDidChangeListener:(FIRIDTokenDidChangeListenerHandle)listenerHandle {
+ [[NSNotificationCenter defaultCenter] removeObserver:listenerHandle];
+ @synchronized (self) {
+ [_listenerHandles removeObject:listenerHandle];
+ }
+}
+
+- (NSData *)APNStoken {
+ __block NSData *result = nil;
+ dispatch_sync(FIRAuthGlobalWorkQueue(), ^{
+ result = _tokenManager.token.data;
+ });
+ return result;
+}
+
+- (void)setAPNSToken:(NSData *)APNSToken {
+ [self setAPNSToken:APNSToken type:FIRAuthAPNSTokenTypeUnknown];
+}
+
+- (void)setAPNSToken:(NSData *)token type:(FIRAuthAPNSTokenType)type {
+ dispatch_sync(FIRAuthGlobalWorkQueue(), ^{
+ _tokenManager.token = [[FIRAuthAPNSToken alloc] initWithData:token type:type];
+ });
+}
+
+- (BOOL)canHandleNotification:(NSDictionary *)userInfo {
+ __block BOOL result = NO;
+ dispatch_sync(FIRAuthGlobalWorkQueue(), ^{
+ result = [_notificationManager canHandleNotification:userInfo];
+ });
+ return result;
+}
+
+#pragma mark - Internal Methods
+
+/** @fn signInWithPhoneCredential:callback:
+ @brief Signs in using a phone credential.
+ @param credential The Phone Auth credential used to sign in.
+ @param callback A block which is invoked when the sign in finishes (or is cancelled.) Invoked
+ asynchronously on the global auth work queue in the future.
+ */
+- (void)signInWithPhoneCredential:(FIRPhoneAuthCredential *)credential
+ callback:(FIRAuthResultCallback)callback {
+ if (credential.temporaryProof.length && credential.phoneNumber.length) {
+ FIRVerifyPhoneNumberRequest *request =
+ [[FIRVerifyPhoneNumberRequest alloc] initWithTemporaryProof:credential.temporaryProof
+ phoneNumber:credential.phoneNumber
+ APIKey:_APIKey];
+ [self phoneNumberSignInWithRequest:request callback:callback];
+ return;
+ }
+
+ if (!credential.verificationID.length) {
+ callback(nil, [FIRAuthErrorUtils missingVerificationIDErrorWithMessage:nil]);
+ return;
+ }
+ if (!credential.verificationCode.length) {
+ callback(nil, [FIRAuthErrorUtils missingVerificationCodeErrorWithMessage:nil]);
+ return;
+ }
+ FIRVerifyPhoneNumberRequest *request =
+ [[FIRVerifyPhoneNumberRequest alloc]initWithVerificationID:credential.verificationID
+ verificationCode:credential.verificationCode
+ APIKey:_APIKey];
+ [self phoneNumberSignInWithRequest:request callback:callback];
+}
+
+
+/** @fn phoneNumberSignInWithVerificationID:pasverificationCodesword:callback:
+ @brief Signs in using a FIRVerifyPhoneNumberRequest object.
+ @param request THe FIRVerifyPhoneNumberRequest request object.
+ @param callback A block which is invoked when the sign in finishes (or is cancelled.) Invoked
+ asynchronously on the global auth work queue in the future.
+ */
+- (void)phoneNumberSignInWithRequest:(FIRVerifyPhoneNumberRequest *)request
+ callback:(FIRAuthResultCallback)callback {
+ [FIRAuthBackend verifyPhoneNumber:request
+ callback:^(FIRVerifyPhoneNumberResponse *_Nullable response,
+ NSError *_Nullable error) {
+ if (error) {
+ callback(nil, error);
+ return;
+ }
+ [self completeSignInWithAccessToken:response.IDToken
+ accessTokenExpirationDate:response.approximateExpirationDate
+ refreshToken:response.refreshToken
+ anonymous:NO
+ callback:callback];
+ }];
+}
+
+- (void)notifyListenersOfAuthStateChangeWithUser:(FIRUser *)user token:(NSString *)token {
+ if (user && _autoRefreshTokens) {
+ // Shedule new refresh task after successful attempt.
+ [self scheduleAutoTokenRefresh];
+ }
+ if (user == _currentUser) {
+ NSMutableDictionary *internalNotificationParameters = [NSMutableDictionary dictionary];
+ if (token.length) {
+ internalNotificationParameters[FIRAuthStateDidChangeInternalNotificationTokenKey] = token;
+ }
+ NSNotificationCenter *notifications = [NSNotificationCenter defaultCenter];
+ dispatch_async(dispatch_get_main_queue(), ^{
+ [notifications postNotificationName:FIRAuthStateDidChangeInternalNotification
+ object:self
+ userInfo:internalNotificationParameters];
+ [notifications postNotificationName:FIRAuthStateDidChangeNotification
+ object:self];
+ });
+ }
+}
+
+- (BOOL)updateKeychainWithUser:(FIRUser *)user error:(NSError *_Nullable *_Nullable)error {
+ if (user == _currentUser) {
+ return [self saveUser:user error:error];
+ }
+ // No-op if the user is no longer signed in. This is not considered an error as we don't check
+ // whether the user is still current on other callbacks of user operations either.
+ return YES;
+}
+
+/** @fn setKeychainServiceNameForApp
+ @brief Sets the keychain service name global data for the particular app.
+ @param app The Firebase app to set keychain service name for.
+ */
++ (void)setKeychainServiceNameForApp:(FIRApp *)app {
+ @synchronized (self) {
+ gKeychainServiceNameForAppName[app.name] =
+ [@"firebase_auth_" stringByAppendingString:app.options.googleAppID];
+ }
+}
+
+/** @fn keychainServiceNameForAppName:
+ @brief Gets the keychain service name global data for the particular app by name.
+ @param appName The name of the Firebase app to get keychain service name for.
+ */
++ (NSString *)keychainServiceNameForAppName:(NSString *)appName {
+ @synchronized (self) {
+ return gKeychainServiceNameForAppName[appName];
+ }
+}
+
+/** @fn deleteKeychainServiceNameForAppName:
+ @brief Deletes the keychain service name global data for the particular app by name.
+ @param appName The name of the Firebase app to delete keychain service name for.
+ */
++ (void)deleteKeychainServiceNameForAppName:(NSString *)appName {
+ @synchronized (self) {
+ [gKeychainServiceNameForAppName removeObjectForKey:appName];
+ }
+}
+
+/** @fn scheduleAutoTokenRefreshWithDelay:
+ @brief Schedules a task to automatically refresh tokens on the current user. The token refresh
+ is scheduled 5 minutes before the scheduled expiration time.
+ @remarks If the token expires in less than 5 minutes, schedule the token refresh immediately.
+ */
+- (void)scheduleAutoTokenRefresh {
+ NSTimeInterval tokenExpirationInterval =
+ [_currentUser.accessTokenExpirationDate timeIntervalSinceNow] - kTokenRefreshHeadStart;
+ [self scheduleAutoTokenRefreshWithDelay:MAX(tokenExpirationInterval, 0) retry:NO];
+}
+
+/** @fn scheduleAutoTokenRefreshWithDelay:
+ @brief Schedules a task to automatically refresh tokens on the current user.
+ @param delay The delay in seconds after which the token refresh task should be scheduled to be
+ executed.
+ @param retry Flag to determine whether the invocation is a retry attempt or not.
+ */
+- (void)scheduleAutoTokenRefreshWithDelay:(NSTimeInterval)delay retry:(BOOL)retry {
+ NSString *accessToken = _currentUser.rawAccessToken;
+ if (!accessToken) {
+ return;
+ }
+ if (retry) {
+ FIRLogNotice(kFIRLoggerAuth, @"I-AUT000003",
+ @"Token auto-refresh re-scheduled in %02d:%02d "
+ @"because of error on previous refresh attempt.",
+ (int)ceil(delay) / 60, (int)ceil(delay) % 60);
+ } else {
+ FIRLogInfo(kFIRLoggerAuth, @"I-AUT000004",
+ @"Token auto-refresh scheduled in %02d:%02d for the new token.",
+ (int)ceil(delay) / 60, (int)ceil(delay) % 60);
+ }
+ _autoRefreshScheduled = YES;
+ __weak FIRAuth *weakSelf = self;
+ [[FIRAuthDispatcher sharedInstance] dispatchAfterDelay:delay
+ queue:FIRAuthGlobalWorkQueue()
+ task:^(void) {
+ FIRAuth *strongSelf = weakSelf;
+ if (!strongSelf) {
+ return;
+ }
+ if (![strongSelf->_currentUser.rawAccessToken isEqualToString:accessToken]) {
+ // Another auto refresh must have been scheduled, so keep _autoRefreshScheduled unchanged.
+ return;
+ }
+ strongSelf->_autoRefreshScheduled = NO;
+ if (strongSelf->_isAppInBackground) {
+ return;
+ }
+ NSString *uid = strongSelf->_currentUser.uid;
+ [strongSelf->_currentUser internalGetTokenForcingRefresh:YES
+ callback:^(NSString *_Nullable token,
+ NSError *_Nullable error) {
+ if (![strongSelf->_currentUser.uid isEqualToString:uid]) {
+ return;
+ }
+ // If the error is an invalid token, sign the user out.
+ if (error.code == FIRAuthErrorCodeInvalidUserToken) {
+ FIRLogWarning(kFIRLoggerAuth, @"I-AUT000005",
+ @"Invalid refresh token detected, user is automatically signed out.");
+ [strongSelf signOutByForceWithUserID:uid error:nil];
+ return;
+ }
+ if (error) {
+ // Kicks off exponential back off logic to retry failed attempt. Starts with one minute
+ // delay (60 seconds) if this is the first failed attempt.
+ NSTimeInterval rescheduleDelay;
+ if (retry) {
+ rescheduleDelay = MIN(delay * 2, kMaxWaitTimeForBackoff);
+ } else {
+ rescheduleDelay = 60;
+ }
+ [strongSelf scheduleAutoTokenRefreshWithDelay:rescheduleDelay retry:YES];
+ }
+ }];
+ }];
+}
+
+#pragma mark -
+
+/** @fn completeSignInWithTokenService:callback:
+ @brief Completes a sign-in flow once we have access and refresh tokens for the user.
+ @param accessToken The STS access token.
+ @param accessTokenExpirationDate The approximate expiration date of the access token.
+ @param refreshToken The STS refresh token.
+ @param anonymous Whether or not the user is anonymous.
+ @param callback Called when the user has been signed in or when an error occurred. Invoked
+ asynchronously on the global auth work queue in the future.
+ */
+- (void)completeSignInWithAccessToken:(NSString *)accessToken
+ accessTokenExpirationDate:(NSDate *)accessTokenExpirationDate
+ refreshToken:(NSString *)refreshToken
+ anonymous:(BOOL)anonymous
+ callback:(FIRAuthResultCallback)callback {
+ [FIRUser retrieveUserWithAPIKey:_APIKey
+ accessToken:accessToken
+ accessTokenExpirationDate:accessTokenExpirationDate
+ refreshToken:refreshToken
+ anonymous:anonymous
+ callback:callback];
+}
+
+/** @fn signInFlowAuthResultCallbackByDecoratingCallback:
+ @brief Creates a FIRAuthResultCallback block which wraps another FIRAuthResultCallback; trying
+ to update the current user before forwarding it's invocations along to a subject block
+ @param callback Called when the user has been updated or when an error has occurred. Invoked
+ asynchronously on the main thread in the future.
+ @return Returns a block that updates the current user.
+ @remarks Typically invoked as part of the complete sign-in flow. For any other uses please
+ consider alternative ways of updating the current user.
+*/
+- (FIRAuthResultCallback)signInFlowAuthResultCallbackByDecoratingCallback:
+ (nullable FIRAuthResultCallback)callback {
+ return ^(FIRUser *_Nullable user, NSError *_Nullable error) {
+ if (error) {
+ if (callback) {
+ dispatch_async(dispatch_get_main_queue(), ^{
+ callback(nil, error);
+ });
+ }
+ return;
+ }
+ if (![self updateCurrentUser:user byForce:NO savingToDisk:YES error:&error]) {
+ if (callback) {
+ dispatch_async(dispatch_get_main_queue(), ^{
+ callback(nil, error);
+ });
+ }
+ return;
+ }
+ if (callback) {
+ dispatch_async(dispatch_get_main_queue(), ^{
+ callback(user, nil);
+ });
+ }
+ };
+}
+
+/** @fn signInFlowAuthDataResultCallbackByDecoratingCallback:
+ @brief Creates a FIRAuthDataResultCallback block which wraps another FIRAuthDataResultCallback;
+ trying to update the current user before forwarding it's invocations along to a subject
+ block.
+ @param callback Called when the user has been updated or when an error has occurred. Invoked
+ asynchronously on the main thread in the future.
+ @return Returns a block that updates the current user.
+ @remarks Typically invoked as part of the complete sign-in flow. For any other uses please
+ consider alternative ways of updating the current user.
+*/
+- (FIRAuthDataResultCallback)signInFlowAuthDataResultCallbackByDecoratingCallback:
+ (nullable FIRAuthDataResultCallback)callback {
+ return ^(FIRAuthDataResult *_Nullable authResult, NSError *_Nullable error) {
+ if (error) {
+ if (callback) {
+ dispatch_async(dispatch_get_main_queue(), ^{
+ callback(nil, error);
+ });
+ }
+ return;
+ }
+ if (![self updateCurrentUser:authResult.user byForce:NO savingToDisk:YES error:&error]) {
+ if (callback) {
+ dispatch_async(dispatch_get_main_queue(), ^{
+ callback(nil, error);
+ });
+ }
+ return;
+ }
+ if (callback) {
+ dispatch_async(dispatch_get_main_queue(), ^{
+ callback(authResult, nil);
+ });
+ }
+ };
+}
+
+#pragma mark - User-Related Methods
+
+/** @fn updateCurrentUser:savingToDisk:
+ @brief Update the current user; initializing the user's internal properties correctly, and
+ optionally saving the user to disk.
+ @remarks This method is called during: sign in and sign out events, as well as during class
+ initialization time. The only time the saveToDisk parameter should be set to NO is during
+ class initialization time because the user was just read from disk.
+ @param user The user to use as the current user (including nil, which is passed at sign out
+ time.)
+ @param saveToDisk Indicates the method should persist the user data to disk.
+ */
+- (BOOL)updateCurrentUser:(FIRUser *)user
+ byForce:(BOOL)force
+ savingToDisk:(BOOL)saveToDisk
+ error:(NSError *_Nullable *_Nullable)error {
+ if (user == _currentUser) {
+ return YES;
+ }
+ BOOL success = YES;
+ if (saveToDisk) {
+ success = [self saveUser:user error:error];
+ }
+ if (success || force) {
+ FIRUser *previousUser = _currentUser;
+ previousUser.auth = nil;
+ _currentUser = user;
+ _currentUser.auth = self;
+ [self notifyListenersOfAuthStateChangeWithUser:user token:user.rawAccessToken];
+ }
+ return success;
+}
+
+/** @fn saveUser:error:
+ @brief Persists user.
+ @param user The user to save.
+ @param error Return value for any error which occurs.
+ @return @YES on success, @NO otherwise.
+ */
+- (BOOL)saveUser:(FIRUser *)user
+ error:(NSError *_Nullable *_Nullable)error {
+ BOOL success;
+ NSString *userKey = [NSString stringWithFormat:kUserKey, _firebaseAppName];
+
+ if (!user) {
+ success = [_keychain removeDataForKey:userKey error:error];
+ } else {
+ // Encode the user object.
+ NSMutableData *archiveData = [NSMutableData data];
+ NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData:archiveData];
+ [archiver encodeObject:user forKey:userKey];
+ [archiver finishEncoding];
+
+ // Save the user object's encoded value.
+ success = [_keychain setData:archiveData forKey:userKey error:error];
+ }
+ return success;
+}
+
+/** @fn getUser:error:
+ @brief Retrieves the saved user associated, if one exists, from the keychain.
+ @param outUser An out parameter which is populated with the saved user, if one exists.
+ @param error Return value for any error which occurs.
+ @return YES if the operation was a success (irrespective of whether or not a saved user existed
+ for the given @c firebaseAppId,) NO if an error occurred.
+ */
+- (BOOL)getUser:(FIRUser *_Nullable *)outUser
+ error:(NSError *_Nullable *_Nullable)error {
+ NSString *userKey = [NSString stringWithFormat:kUserKey, _firebaseAppName];
+
+ NSError *keychainError;
+ NSData *encodedUserData = [_keychain dataForKey:userKey error:&keychainError];
+ if (keychainError) {
+ if (error) {
+ *error = keychainError;
+ }
+ return NO;
+ }
+ if (!encodedUserData) {
+ *outUser = nil;
+ return YES;
+ }
+ NSKeyedUnarchiver *unarchiver =
+ [[NSKeyedUnarchiver alloc] initForReadingWithData:encodedUserData];
+ *outUser = [unarchiver decodeObjectOfClass:[FIRUser class] forKey:userKey];
+ return YES;
+}
+
+/** @fn getUID
+ @brief Gets the identifier of the current user, if any.
+ @return The identifier of the current user, or nil if there is no current user.
+ */
+- (nullable NSString *)getUID {
+ return _currentUser.uid;
+}
+
+@end
diff --git a/Firebase/Auth/Source/FIRAuthAPNSToken.m b/Firebase/Auth/Source/FIRAuthAPNSToken.m
new file mode 100644
index 0000000..fc5ee2d
--- /dev/null
+++ b/Firebase/Auth/Source/FIRAuthAPNSToken.m
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "Private/FIRAuthAPNSToken.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@implementation FIRAuthAPNSToken
+
+- (instancetype)initWithData:(NSData *)data type:(FIRAuthAPNSTokenType)type {
+ self = [super init];
+ if (self) {
+ _data = [data copy];
+ _type = type;
+ }
+ return self;
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/FIRAuthAPNSTokenManager.m b/Firebase/Auth/Source/FIRAuthAPNSTokenManager.m
new file mode 100644
index 0000000..9609d86
--- /dev/null
+++ b/Firebase/Auth/Source/FIRAuthAPNSTokenManager.m
@@ -0,0 +1,255 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "Private/FIRAuthAPNSTokenManager.h"
+
+#import "FIRLogger.h"
+#import "Private/FIRAuthAPNSToken.h"
+#import "FIRAuthGlobalWorkQueue.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @var kRegistrationTimeout
+ @brief Timeout for registration for remote notification.
+ @remarks Once we start to handle `application:didFailToRegisterForRemoteNotificationsWithError:`
+ we probably don't have to use timeout at all.
+ */
+static const NSTimeInterval kRegistrationTimeout = 5;
+
+/** @var kLegacyRegistrationTimeout
+ @brief Timeout for registration for remote notification on iOS 7.
+ */
+static const NSTimeInterval kLegacyRegistrationTimeout = 30;
+
+@implementation FIRAuthAPNSTokenManager {
+ /** @var _application
+ @brief The @c UIApplication to request the token from.
+ */
+ UIApplication *_application;
+
+ /** @var _pendingCallbacks
+ @brief The list of all pending callbacks for the APNs token.
+ */
+ NSMutableArray<FIRAuthAPNSTokenCallback> *_pendingCallbacks;
+}
+
+- (instancetype)initWithApplication:(UIApplication *)application {
+ self = [super init];
+ if (self) {
+ _application = application;
+ _timeout = [_application respondsToSelector:@selector(registerForRemoteNotifications)] ?
+ kRegistrationTimeout : kLegacyRegistrationTimeout;
+ }
+ return self;
+}
+
+- (void)getTokenWithCallback:(FIRAuthAPNSTokenCallback)callback {
+ if (_token) {
+ callback(_token);
+ return;
+ }
+ if (_pendingCallbacks) {
+ [_pendingCallbacks addObject:callback];
+ return;
+ }
+ _pendingCallbacks =
+ [[NSMutableArray<FIRAuthAPNSTokenCallback> alloc] initWithObjects:callback, nil];
+ dispatch_async(dispatch_get_main_queue(), ^{
+ if ([_application respondsToSelector:@selector(registerForRemoteNotifications)]) {
+ [_application registerForRemoteNotifications];
+ } else {
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdeprecated-declarations"
+ [_application registerForRemoteNotificationTypes:UIRemoteNotificationTypeAlert];
+#pragma clang diagnostic pop
+ }
+ });
+ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(_timeout * NSEC_PER_SEC)),
+ FIRAuthGlobalWorkQueue(), ^{
+ [self callBack];
+ });
+}
+
+- (void)setToken:(nullable FIRAuthAPNSToken *)token {
+ if (!token) {
+ _token = nil;
+ return;
+ }
+ if (token.type == FIRAuthAPNSTokenTypeUnknown) {
+ static FIRAuthAPNSTokenType detectedTokenType = FIRAuthAPNSTokenTypeUnknown;
+ if (detectedTokenType == FIRAuthAPNSTokenTypeUnknown) {
+ detectedTokenType =
+ [[self class] isProductionApp] ? FIRAuthAPNSTokenTypeProd : FIRAuthAPNSTokenTypeSandbox;
+ }
+ token = [[FIRAuthAPNSToken alloc] initWithData:token.data type:detectedTokenType];
+ }
+ _token = token;
+ [self callBack];
+}
+
+#pragma mark - Internal methods
+
+/** @fn callBack
+ @brief Calls back all pending callbacks with the current APNs token, if one is available.
+ */
+- (void)callBack {
+ if (!_pendingCallbacks) {
+ return;
+ }
+ NSArray<FIRAuthAPNSTokenCallback> *allCallbacks = _pendingCallbacks;
+ _pendingCallbacks = nil;
+ for (FIRAuthAPNSTokenCallback callback in allCallbacks) {
+ callback(_token);
+ }
+};
+
+/** @fn isProductionApp
+ @brief Whether or not the app has production (versus sandbox) provisioning profile.
+ @remarks This method is adapted from @c FIRInstanceID .
+ */
++ (BOOL)isProductionApp {
+ const BOOL defaultAppTypeProd = YES;
+
+ NSError *error = nil;
+
+ Class envClass = NSClassFromString(@"FIRAppEnvironmentUtil");
+ SEL isSimulatorSelector = NSSelectorFromString(@"isSimulator");
+ if ([envClass respondsToSelector:isSimulatorSelector]) {
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
+ if ([envClass performSelector:isSimulatorSelector]) {
+#pragma clang diagnostic pop
+ FIRLogWarning(kFIRLoggerAuth, @"I-AUT000006",
+ @"Assuming prod APNs token type on simulator.");
+ return defaultAppTypeProd;
+ }
+ }
+
+ NSString *path = [[[NSBundle mainBundle] bundlePath]
+ stringByAppendingPathComponent:@"embedded.mobileprovision"];
+
+ // Apps distributed via AppStore or TestFlight use the Production APNS certificates.
+ SEL isFromAppStoreSelector = NSSelectorFromString(@"isFromAppStore");
+ if ([envClass respondsToSelector:isFromAppStoreSelector]) {
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
+ if ([envClass performSelector:isFromAppStoreSelector]) {
+#pragma clang diagnostic pop
+ return defaultAppTypeProd;
+ }
+ }
+
+ SEL isAppStoreReceiptSandboxSelector = NSSelectorFromString(@"isAppStoreReceiptSandbox");
+ if ([envClass respondsToSelector:isAppStoreReceiptSandboxSelector]) {
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
+ if ([envClass performSelector:isAppStoreReceiptSandboxSelector] && !path.length) {
+#pragma clang diagnostic pop
+ // Distributed via TestFlight
+ return defaultAppTypeProd;
+ }
+ }
+
+ NSMutableData *profileData = [NSMutableData dataWithContentsOfFile:path options:0 error:&error];
+
+ if (!profileData.length || error) {
+ FIRLogWarning(kFIRLoggerAuth, @"I-AUT000007",
+ @"Error while reading embedded mobileprovision %@", error);
+ return defaultAppTypeProd;
+ }
+
+ // The "embedded.mobileprovision" sometimes contains characters with value 0, which signals the
+ // end of a c-string and halts the ASCII parser, or with value > 127, which violates strict 7-bit
+ // ASCII. Replace any 0s or invalid characters in the input.
+ uint8_t *profileBytes = (uint8_t *)profileData.bytes;
+ for (int i = 0; i < profileData.length; i++) {
+ uint8_t currentByte = profileBytes[i];
+ if (!currentByte || currentByte > 127) {
+ profileBytes[i] = '.';
+ }
+ }
+
+ NSString *embeddedProfile = [[NSString alloc] initWithBytesNoCopy:profileBytes
+ length:profileData.length
+ encoding:NSASCIIStringEncoding
+ freeWhenDone:NO];
+
+ if (error || !embeddedProfile.length) {
+ FIRLogWarning(kFIRLoggerAuth, @"I-AUT000008",
+ @"Error while reading embedded mobileprovision %@", error);
+ return defaultAppTypeProd;
+ }
+
+ NSScanner *scanner = [NSScanner scannerWithString:embeddedProfile];
+ NSString *plistContents;
+ if ([scanner scanUpToString:@"<plist" intoString:nil]) {
+ if ([scanner scanUpToString:@"</plist>" intoString:&plistContents]) {
+ plistContents = [plistContents stringByAppendingString:@"</plist>"];
+ }
+ }
+
+ if (!plistContents.length) {
+ return defaultAppTypeProd;
+ }
+
+ NSData *data = [plistContents dataUsingEncoding:NSUTF8StringEncoding];
+ if (!data.length) {
+ FIRLogWarning(kFIRLoggerAuth, @"I-AUT000009",
+ @"Couldn't read plist fetched from embedded mobileprovision");
+ return defaultAppTypeProd;
+ }
+
+ NSError *plistMapError;
+ id plistData = [NSPropertyListSerialization propertyListWithData:data
+ options:NSPropertyListImmutable
+ format:nil
+ error:&plistMapError];
+ if (plistMapError || ![plistData isKindOfClass:[NSDictionary class]]) {
+ FIRLogWarning(kFIRLoggerAuth, @"I-AUT000010",
+ @"Error while converting assumed plist to dict %@",
+ plistMapError.localizedDescription);
+ return defaultAppTypeProd;
+ }
+ NSDictionary *plistMap = (NSDictionary *)plistData;
+
+ if ([plistMap valueForKeyPath:@"ProvisionedDevices"]) {
+ FIRLogInfo(kFIRLoggerAuth, @"I-AUT000011",
+ @"Provisioning profile has specifically provisioned devices, "
+ @"most likely a Dev profile.");
+ }
+
+ NSString *apsEnvironment = [plistMap valueForKeyPath:@"Entitlements.aps-environment"];
+ FIRLogDebug(kFIRLoggerAuth, @"I-AUT000012",
+ @"APNS Environment in profile: %@", apsEnvironment);
+
+ // No aps-environment in the profile.
+ if (!apsEnvironment.length) {
+ FIRLogWarning(kFIRLoggerAuth, @"I-AUT000013",
+ @"No aps-environment set. If testing on a device APNS is not "
+ @"correctly configured. Please recheck your provisioning profiles.");
+ return defaultAppTypeProd;
+ }
+
+ if ([apsEnvironment isEqualToString:@"development"]) {
+ return NO;
+ }
+
+ return defaultAppTypeProd;
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/FIRAuthAPNSTokenType.h b/Firebase/Auth/Source/FIRAuthAPNSTokenType.h
new file mode 100644
index 0000000..87df574
--- /dev/null
+++ b/Firebase/Auth/Source/FIRAuthAPNSTokenType.h
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+#import "FIRAuthSwiftNameSupport.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * @brief The APNs token type for the app.
+ */
+typedef NS_ENUM(NSInteger, FIRAuthAPNSTokenType) {
+
+ /** Unknown token type.
+ The actual token type will be detected from the provisioning profile in the app's bundle.
+ */
+ FIRAuthAPNSTokenTypeUnknown,
+
+ /** Sandbox token type.
+ */
+ FIRAuthAPNSTokenTypeSandbox,
+
+ /** Production token type.
+ */
+ FIRAuthAPNSTokenTypeProd,
+} FIR_SWIFT_NAME(AuthAPNSTokenType);
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/FIRAuthAppCredential.m b/Firebase/Auth/Source/FIRAuthAppCredential.m
new file mode 100644
index 0000000..ad12741
--- /dev/null
+++ b/Firebase/Auth/Source/FIRAuthAppCredential.m
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "Private/FIRAuthAppCredential.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @var kReceiptKey
+ @brief The key used to encode the receipt property for NSSecureCoding.
+ */
+static NSString *const kReceiptKey = @"receipt";
+
+/** @var kSecretKey
+ @brief The key used to encode the secret property for NSSecureCoding.
+ */
+static NSString *const kSecretKey = @"secret";
+
+@implementation FIRAuthAppCredential
+
+- (instancetype)initWithReceipt:(NSString *)receipt secret:(nullable NSString *)secret {
+ self = [super init];
+ if (self) {
+ _receipt = [receipt copy];
+ _secret = [secret copy];
+ }
+ return self;
+}
+
+#pragma mark - NSSecureCoding
+
++ (BOOL)supportsSecureCoding {
+ return YES;
+}
+
+- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder {
+ NSString *receipt = [aDecoder decodeObjectOfClass:[NSString class] forKey:kReceiptKey];
+ if (!receipt) {
+ return nil;
+ }
+ NSString *secret = [aDecoder decodeObjectOfClass:[NSString class] forKey:kSecretKey];
+ return [self initWithReceipt:receipt secret:secret];
+}
+
+- (void)encodeWithCoder:(NSCoder *)aCoder {
+ [aCoder encodeObject:_receipt forKey:kReceiptKey];
+ [aCoder encodeObject:_secret forKey:kSecretKey];
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/FIRAuthAppCredentialManager.m b/Firebase/Auth/Source/FIRAuthAppCredentialManager.m
new file mode 100644
index 0000000..2a0d1c7
--- /dev/null
+++ b/Firebase/Auth/Source/FIRAuthAppCredentialManager.m
@@ -0,0 +1,164 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "Private/FIRAuthAppCredentialManager.h"
+
+#import "Private/FIRAuthAppCredential.h"
+#import "FIRAuthGlobalWorkQueue.h"
+#import "FIRAuthKeychain.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @var kKeychainDataKey
+ @brief The keychain key for the data.
+ */
+static NSString *const kKeychainDataKey = @"app_credentials";
+
+/** @var kFullCredentialKey
+ @brief The data key for the full app credential.
+ */
+static NSString *const kFullCredentialKey = @"full_credential";
+
+/** @var kPendingReceiptsKey
+ @brief The data key for the array of pending receipts.
+ */
+static NSString *const kPendingReceiptsKey = @"pending_receipts";
+
+/** @var kMaximumNumberOfPendingReceipts
+ @brief The maximum number of partial credentials kept by this class.
+ */
+static const NSUInteger kMaximumNumberOfPendingReceipts = 32;
+
+@implementation FIRAuthAppCredentialManager {
+ /** @var _keychain
+ @brief The keychain for app credentials to load from and to save to.
+ */
+ FIRAuthKeychain *_keychain;
+
+ /** @var _pendingReceipts
+ @brief A list of pending receipts sorted in the order they were recorded.
+ */
+ NSMutableArray<NSString *> *_pendingReceipts;
+
+ /** @var _callbacksByReceipt
+ @brief A map from pending receipts to callbacks.
+ */
+ NSMutableDictionary<NSString *, FIRAuthAppCredentialCallback> *_callbacksByReceipt;
+}
+
+- (instancetype)initWithKeychain:(FIRAuthKeychain *)keychain {
+ self = [super init];
+ if (self) {
+ _keychain = keychain;
+ // Load the credentials from keychain if possible.
+ NSError *error;
+ NSData *encodedData = [_keychain dataForKey:kKeychainDataKey error:&error];
+ if (!error && encodedData) {
+ NSKeyedUnarchiver *unarchiver =
+ [[NSKeyedUnarchiver alloc] initForReadingWithData:encodedData];
+ FIRAuthAppCredential *credential =
+ [unarchiver decodeObjectOfClass:[FIRAuthAppCredential class]
+ forKey:kFullCredentialKey];
+ if ([credential isKindOfClass:[FIRAuthAppCredential class]]) {
+ _credential = credential;
+ }
+ NSSet<Class> *allowedClasses =
+ [NSSet<Class> setWithObjects:[NSArray class], [NSString class], nil];
+ NSArray<NSString *> *pendingReceipts =
+ [unarchiver decodeObjectOfClasses:allowedClasses forKey:kPendingReceiptsKey];
+ if ([pendingReceipts isKindOfClass:[NSArray class]]) {
+ _pendingReceipts = [pendingReceipts mutableCopy];
+ }
+ }
+ if (!_pendingReceipts) {
+ _pendingReceipts = [[NSMutableArray<NSString *> alloc] init];
+ }
+ _callbacksByReceipt =
+ [[NSMutableDictionary<NSString *, FIRAuthAppCredentialCallback> alloc] init];
+ }
+ return self;
+}
+
+- (NSUInteger)maximumNumberOfPendingReceipts {
+ return kMaximumNumberOfPendingReceipts;
+}
+
+- (void)didStartVerificationWithReceipt:(NSString *)receipt
+ timeout:(NSTimeInterval)timeout
+ callback:(FIRAuthAppCredentialCallback)callback {
+ [_pendingReceipts removeObject:receipt];
+ if (_pendingReceipts.count >= kMaximumNumberOfPendingReceipts) {
+ [_pendingReceipts removeObjectAtIndex:0];
+ }
+ [_pendingReceipts addObject:receipt];
+ _callbacksByReceipt[receipt] = callback;
+ [self saveData];
+ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeout * NSEC_PER_SEC)),
+ FIRAuthGlobalWorkQueue(), ^{
+ [self callBackWithReceipt:receipt];
+ });
+}
+
+- (BOOL)canFinishVerificationWithReceipt:(NSString *)receipt secret:(NSString *)secret {
+ if (![_pendingReceipts containsObject:receipt]) {
+ return NO;
+ }
+ [_pendingReceipts removeObject:receipt];
+ _credential = [[FIRAuthAppCredential alloc] initWithReceipt:receipt secret:secret];
+ [self saveData];
+ [self callBackWithReceipt:receipt];
+ return YES;
+}
+
+- (void)clearCredential {
+ _credential = nil;
+ [self saveData];
+}
+
+#pragma mark - Internal methods
+
+/** @fn saveData
+ @brief Save the data in memory to the keychain ignoring any errors.
+ */
+- (void)saveData {
+ NSMutableData *archiveData = [NSMutableData data];
+ NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData:archiveData];
+ [archiver encodeObject:_credential forKey:kFullCredentialKey];
+ [archiver encodeObject:_pendingReceipts forKey:kPendingReceiptsKey];
+ [archiver finishEncoding];
+ [_keychain setData:archiveData forKey:kKeychainDataKey error:NULL];
+}
+
+/** @fn callBackWithReceipt:
+ @brief Calls the saved callback for the specifc receipt.
+ @param receipt The receipt associated with the callback.
+ */
+- (void)callBackWithReceipt:(NSString *)receipt {
+ FIRAuthAppCredentialCallback callback = _callbacksByReceipt[receipt];
+ if (!callback) {
+ return;
+ }
+ [_callbacksByReceipt removeObjectForKey:receipt];
+ if (_credential) {
+ callback(_credential);
+ } else {
+ callback([[FIRAuthAppCredential alloc] initWithReceipt:receipt secret:nil]);
+ }
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/FIRAuthAppDelegateProxy.m b/Firebase/Auth/Source/FIRAuthAppDelegateProxy.m
new file mode 100644
index 0000000..ca88a4c
--- /dev/null
+++ b/Firebase/Auth/Source/FIRAuthAppDelegateProxy.m
@@ -0,0 +1,245 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "Private/FIRAuthAppDelegateProxy.h"
+
+#import <objc/runtime.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @var kProxyEnabledBundleKey
+ @brief The key in application's bundle plist for whether or not proxy should be enabled.
+ @remarks This key is a shared constant with Analytics and FCM.
+ */
+static NSString *const kProxyEnabledBundleKey = @"FirebaseAppDelegateProxyEnabled";
+
+/** @fn noop
+ @brief A function that does nothing.
+ @remarks This is used as the placeholder for unimplemented UApplicationDelegate methods,
+ because once we added a method there is no way to remove it from the class.
+ */
+#if !OBJC_OLD_DISPATCH_PROTOTYPES
+static void noop(void) {
+}
+#else
+static id noop(id object, SEL cmd, ...) {
+ return nil;
+}
+#endif
+
+@implementation FIRAuthAppDelegateProxy {
+ /** @var _appDelegate
+ @brief The application delegate whose method is being swizzled.
+ */
+ id<UIApplicationDelegate> _appDelegate;
+
+ /** @var _orginalImplementationsBySelector
+ @brief A map from selectors to original implementations that have been swizzled.
+ */
+ NSMutableDictionary<NSValue *, NSValue *> *_originalImplementationsBySelector;
+
+ /** @var _handlers
+ @brief The array of weak pointers of `id<FIRAuthAppDelegateHandler>`.
+ */
+ NSPointerArray *_handlers;
+}
+
+- (nullable instancetype)initWithApplication:(nullable UIApplication *)application {
+ self = [super init];
+ if (self) {
+ id proxyEnabled = [[NSBundle mainBundle] objectForInfoDictionaryKey:kProxyEnabledBundleKey];
+ if ([proxyEnabled isKindOfClass:[NSNumber class]] && !((NSNumber *)proxyEnabled).boolValue) {
+ return nil;
+ }
+ _appDelegate = application.delegate;
+ if (![_appDelegate conformsToProtocol:@protocol(UIApplicationDelegate)]) {
+ return nil;
+ }
+ _originalImplementationsBySelector = [[NSMutableDictionary<NSValue *, NSValue *> alloc] init];
+ _handlers = [[NSPointerArray alloc] initWithOptions:NSPointerFunctionsWeakMemory];
+
+ // Swizzle the methods.
+ __weak FIRAuthAppDelegateProxy *weakSelf = self;
+ SEL registerDeviceTokenSelector =
+ @selector(application:didRegisterForRemoteNotificationsWithDeviceToken:);
+ [self replaceSelector:registerDeviceTokenSelector
+ withBlock:^(id object, UIApplication* application, NSData *deviceToken) {
+ [weakSelf object:object
+ selector:registerDeviceTokenSelector
+ application:application
+ didRegisterForRemoteNotificationsWithDeviceToken:deviceToken];
+ }];
+ SEL receiveNotificationSelector = @selector(application:didReceiveRemoteNotification:);
+ SEL receiveNotificationWithHandlerSelector =
+ @selector(application:didReceiveRemoteNotification:fetchCompletionHandler:);
+ if ([_appDelegate respondsToSelector:receiveNotificationWithHandlerSelector] ||
+ ![_appDelegate respondsToSelector:receiveNotificationSelector]) {
+ // Replace the modern selector which is available on iOS 7 and above.
+ [self replaceSelector:receiveNotificationWithHandlerSelector
+ withBlock:^(id object, UIApplication *application, NSDictionary *notification,
+ void (^completionHandler)(UIBackgroundFetchResult)) {
+ [weakSelf object:object
+ selector:receiveNotificationWithHandlerSelector
+ application:application
+ didReceiveRemoteNotification:notification
+ fetchCompletionHandler:completionHandler];
+ }];
+ } else {
+ // Replace the deprecated selector because this is the only one that the client app uses.
+ [self replaceSelector:receiveNotificationSelector
+ withBlock:^(id object, UIApplication *application, NSDictionary *notification) {
+ [weakSelf object:object
+ selector:receiveNotificationSelector
+ application:application
+ didReceiveRemoteNotification:notification];
+ }];
+ }
+ }
+ return self;
+}
+
+- (void)dealloc {
+ for (NSValue *selector in _originalImplementationsBySelector) {
+ IMP implementation = _originalImplementationsBySelector[selector].pointerValue;
+ Method method = class_getInstanceMethod([_appDelegate class], selector.pointerValue);
+ imp_removeBlock(method_setImplementation(method, implementation));
+ }
+}
+
+- (void)addHandler:(__weak id<FIRAuthAppDelegateHandler>)handler {
+ @synchronized (_handlers) {
+ [_handlers addPointer:(__bridge void *)handler];
+ }
+}
+
++ (nullable instancetype)sharedInstance {
+ static dispatch_once_t onceToken;
+ static FIRAuthAppDelegateProxy *_Nullable sharedInstance;
+ dispatch_once(&onceToken, ^{
+ sharedInstance = [[self alloc] initWithApplication:[UIApplication sharedApplication]];
+ });
+ return sharedInstance;
+}
+
+#pragma mark - UIApplicationDelegate proxy methods.
+
+- (void)object:(id)object
+ selector:(SEL)selector
+ application:(UIApplication *)application
+ didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {
+ if (object == _appDelegate) {
+ for (id<FIRAuthAppDelegateHandler> handler in [self handlers]) {
+ [handler setAPNSToken:deviceToken];
+ }
+ }
+ IMP originalImplementation = [self originalImplementationForSelector:selector];
+ if (originalImplementation) {
+ typedef void (*Implmentation)(id, SEL, UIApplication*, NSData *);
+ ((Implmentation)originalImplementation)(object, selector, application, deviceToken);
+ }
+}
+
+- (void)object:(id)object
+ selector:(SEL)selector
+ application:(UIApplication *)application
+ didReceiveRemoteNotification:(NSDictionary *)notification
+ fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler {
+ if (object == _appDelegate) {
+ for (id<FIRAuthAppDelegateHandler> handler in [self handlers]) {
+ if ([handler canHandleNotification:notification]) {
+ completionHandler(UIBackgroundFetchResultNoData);
+ return;
+ };
+ }
+ }
+ IMP originalImplementation = [self originalImplementationForSelector:selector];
+ typedef void (*Implmentation)(id, SEL, UIApplication*, NSDictionary *,
+ void (^)(UIBackgroundFetchResult));
+ ((Implmentation)originalImplementation)(object, selector, application, notification,
+ completionHandler);
+}
+
+- (void)object:(id)object
+ selector:(SEL)selector
+ application:(UIApplication *)application
+ didReceiveRemoteNotification:(NSDictionary *)notification {
+ if (object == _appDelegate) {
+ for (id<FIRAuthAppDelegateHandler> handler in [self handlers]) {
+ if ([handler canHandleNotification:notification]) {
+ return;
+ };
+ }
+ }
+ IMP originalImplementation = [self originalImplementationForSelector:selector];
+ typedef void (*Implmentation)(id, SEL, UIApplication*, NSDictionary *);
+ ((Implmentation)originalImplementation)(object, selector, application, notification);
+}
+
+#pragma mark - Internal Methods
+
+/** @fn handlers
+ @brief Gets the list of handlers from `_handlers` safely.
+ */
+- (NSArray<id<FIRAuthAppDelegateHandler>> *)handlers {
+ @synchronized (_handlers) {
+ NSMutableArray<id<FIRAuthAppDelegateHandler>> *liveHandlers =
+ [[NSMutableArray<id<FIRAuthAppDelegateHandler>> alloc] initWithCapacity:_handlers.count];
+ for (__weak id<FIRAuthAppDelegateHandler> handler in _handlers) {
+ if (handler) {
+ [liveHandlers addObject:handler];
+ }
+ }
+ if (liveHandlers.count < _handlers.count) {
+ [_handlers compact];
+ }
+ return liveHandlers;
+ }
+}
+
+/** @fn replaceSelector:withBlock:
+ @brief replaces the implementation for a method of `_appDelegate` specified by a selector.
+ @param selector The selector for the method.
+ @param block The block as the new implementation of the method.
+ */
+- (void)replaceSelector:(SEL)selector withBlock:(id)block {
+ Method originalMethod = class_getInstanceMethod([_appDelegate class], selector);
+ IMP newImplementation = imp_implementationWithBlock(block);
+ IMP originalImplementation;
+ if (originalMethod) {
+ originalImplementation = method_setImplementation(originalMethod, newImplementation) ?: &noop;
+ } else {
+ // The original method was not implemented in the class, add it with the new implementation.
+ struct objc_method_description methodDescription =
+ protocol_getMethodDescription(@protocol(UIApplicationDelegate), selector, NO, YES);
+ class_addMethod([_appDelegate class], selector, newImplementation, methodDescription.types);
+ originalImplementation = &noop;
+ }
+ _originalImplementationsBySelector[[NSValue valueWithPointer:selector]] =
+ [NSValue valueWithPointer:originalImplementation];
+}
+
+/** @fn originalImplementationForSelector:
+ @brief Gets the original implementation for the given selector.
+ @param selector The selector for the method that has been replaced.
+ @return The original implementation if there was one.
+ */
+- (IMP)originalImplementationForSelector:(SEL)selector {
+ return _originalImplementationsBySelector[[NSValue valueWithPointer:selector]].pointerValue;
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/FIRAuthCredential.h b/Firebase/Auth/Source/FIRAuthCredential.h
new file mode 100644
index 0000000..ce28854
--- /dev/null
+++ b/Firebase/Auth/Source/FIRAuthCredential.h
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+#import "FIRAuthSwiftNameSupport.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @class FIRAuthCredential
+ @brief Represents a credential.
+ */
+FIR_SWIFT_NAME(AuthCredential)
+@interface FIRAuthCredential : NSObject
+
+/** @property provider
+ @brief Gets the name of the identity provider for the credential.
+ */
+@property(nonatomic, copy, readonly) NSString *provider;
+
+/** @fn init
+ @brief This is an abstract base class. Concrete instances should be created via factory
+ methods available in the various authentication provider libraries (like the Facebook
+ provider or the Google provider libraries.)
+ */
+- (instancetype)init NS_UNAVAILABLE;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/FIRAuthCredential.m b/Firebase/Auth/Source/FIRAuthCredential.m
new file mode 100644
index 0000000..d476d6d
--- /dev/null
+++ b/Firebase/Auth/Source/FIRAuthCredential.m
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "Private/FIRAuthCredential_Internal.h"
+
+@implementation FIRAuthCredential
+
+- (instancetype)init {
+ @throw [NSException exceptionWithName:@"Attempt to call unavailable initializer."
+ reason:@"This class is an abstract base class. It's init method "
+ "should not be called directly."
+ userInfo:nil];
+}
+
+- (nullable instancetype)initWithProvider:(NSString *)provider {
+ self = [super init];
+ if (self) {
+ _provider = [provider copy];
+ }
+ return self;
+}
+
+- (void)prepareVerifyAssertionRequest:(FIRVerifyAssertionRequest *)request {
+ @throw [NSException exceptionWithName:@"Attempt to call virtual method."
+ reason:@"This method must be overridden by a subclass."
+ userInfo:nil];
+}
+
+@end
diff --git a/Firebase/Auth/Source/FIRAuthDataResult.h b/Firebase/Auth/Source/FIRAuthDataResult.h
new file mode 100644
index 0000000..e72adf2
--- /dev/null
+++ b/Firebase/Auth/Source/FIRAuthDataResult.h
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+#import "FIRAuthSwiftNameSupport.h"
+
+@class FIRAdditionalUserInfo;
+@class FIRUser;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @class FIRAuthDataResult
+ @brief Helper object that contains the result of a successful sign-in, link and reauthenticate.
+ It contains a reference to a @c FIRUser and @c FIRAdditionalUserInfo.
+ */
+FIR_SWIFT_NAME(AuthDataResult)
+@interface FIRAuthDataResult : NSObject
+
+/** @fn init
+ @brief This class should not be initialized manually. @c FIRAuthDataResult instance is
+ returned as part of @c FIRAuthDataResultCallback .
+ */
+- (instancetype)init NS_UNAVAILABLE;
+
+/** @property user
+ @brief The signed in user.
+ */
+@property(nonatomic, readonly) FIRUser *user;
+
+/** @property additionalUserInfo
+ @brief If available contains the additional IdP specific information about signed in user.
+ */
+@property(nonatomic, readonly, nullable) FIRAdditionalUserInfo *additionalUserInfo;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/FIRAuthDataResult.m b/Firebase/Auth/Source/FIRAuthDataResult.m
new file mode 100644
index 0000000..fd2daa7
--- /dev/null
+++ b/Firebase/Auth/Source/FIRAuthDataResult.m
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "Private/FIRAuthDataResult_Internal.h"
+
+#import "FIRAdditionalUserInfo.h"
+#import "FIRUser.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@implementation FIRAuthDataResult
+
+/** @var kAdditionalUserInfoCodingKey
+ @brief The key used to encode the additionalUserInfo property for NSSecureCoding.
+ */
+static NSString *const kAdditionalUserInfoCodingKey = @"additionalUserInfo";
+
+/** @var kUserCodingKey
+ @brief The key used to encode the user property for NSSecureCoding.
+ */
+static NSString *const kUserCodingKey = @"user";
+
+- (nullable instancetype)initWithUser:(FIRUser *)user
+ additionalUserInfo:(nullable FIRAdditionalUserInfo *)additionalUserInfo {
+ self = [super init];
+ if (self) {
+ _additionalUserInfo = additionalUserInfo;
+ _user = user;
+ }
+ return self;
+}
+
+#pragma mark - NSSecureCoding
+
++ (BOOL)supportsSecureCoding {
+ return YES;
+}
+
+- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder {
+ FIRUser *user =
+ [aDecoder decodeObjectOfClass:[FIRUser class] forKey:kUserCodingKey];
+ FIRAdditionalUserInfo *additionalUserInfo =
+ [aDecoder decodeObjectOfClass:[FIRAdditionalUserInfo class]
+ forKey:kAdditionalUserInfoCodingKey];
+
+ return [self initWithUser:user additionalUserInfo:additionalUserInfo];
+}
+
+- (void)encodeWithCoder:(NSCoder *)aCoder {
+ [aCoder encodeObject:_user forKey:kUserCodingKey];
+ [aCoder encodeObject:_additionalUserInfo forKey:kAdditionalUserInfoCodingKey];
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/FIRAuthDispatcher.m b/Firebase/Auth/Source/FIRAuthDispatcher.m
new file mode 100644
index 0000000..98eb50a
--- /dev/null
+++ b/Firebase/Auth/Source/FIRAuthDispatcher.m
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "Private/FIRAuthDispatcher.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@implementation FIRAuthDispatcher
+
+@synthesize dispatchAfterImplementation = _dispatchAfterImplementation;
+
++ (instancetype)sharedInstance {
+ static dispatch_once_t onceToken;
+ static FIRAuthDispatcher *sharedInstance;
+ dispatch_once(&onceToken, ^{
+ sharedInstance = [[self alloc] init];
+ });
+ return sharedInstance;
+}
+
+- (void)dispatchAfterDelay:(NSTimeInterval)delay
+ queue:(dispatch_queue_t)queue
+ task:(void (^)(void))task {
+ if (_dispatchAfterImplementation) {
+ _dispatchAfterImplementation(delay, queue, task);
+ return;
+ }
+ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, delay * NSEC_PER_SEC), queue, task);
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/FIRAuthErrorUtils.m b/Firebase/Auth/Source/FIRAuthErrorUtils.m
new file mode 100644
index 0000000..8179d02
--- /dev/null
+++ b/Firebase/Auth/Source/FIRAuthErrorUtils.m
@@ -0,0 +1,794 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "Private/FIRAuthErrorUtils.h"
+
+#import "FIRAuthCredential.h"
+#import "Private/FIRAuthInternalErrors.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+NSString *const FIRAuthErrorDomain = @"FIRAuthErrorDomain";
+
+NSString *const FIRAuthInternalErrorDomain = @"FIRAuthInternalErrorDomain";
+
+NSString *const FIRAuthErrorUserInfoDeserializedResponseKey =
+ @"FIRAuthErrorUserInfoDeserializedResponseKey";
+
+NSString *const FIRAuthErrorUserInfoDataKey = @"FIRAuthErrorUserInfoDataKey";
+
+NSString *const FIRAuthErrorUserInfoEmailKey = @"FIRAuthErrorUserInfoEmailKey";
+
+NSString *const FIRAuthErrorNameKey = @"error_name";
+
+NSString *const FIRAuthUpdatedCredentialKey = @"FIRAuthUpdatedCredentialKey";
+
+/** @var kServerErrorDetailMarker
+ @brief This marker indicates that the server error message contains a detail error message which
+ should be used instead of the hardcoded client error message.
+ */
+static NSString *const kServerErrorDetailMarker = @" : ";
+
+#pragma mark - Standard Error Messages
+
+/** @var kFIRAuthErrorMessageInvalidCustomToken
+ @brief Message for @c FIRAuthErrorCodeInvalidCustomToken error code.
+ */
+static NSString *const kFIRAuthErrorMessageInvalidCustomToken = @"The custom token format is "
+ "incorrect. Please check the documentation.";
+
+/** @var kFIRAuthErrorMessageCustomTokenMismatch
+ @brief Message for @c FIRAuthErrorCodeCustomTokenMismatch error code.
+ */
+static NSString *const kFIRAuthErrorMessageCustomTokenMismatch = @"The custom token corresponds to "
+ "a different audience.";
+
+/** @var kFIRAuthErrorMessageInvalidEmail
+ @brief Message for @c FIRAuthErrorCodeInvalidEmail error code.
+ */
+static NSString *const kFIRAuthErrorMessageInvalidEmail = @"The email address is badly formatted.";
+
+/** @var kFIRAuthErrorMessageInvalidCredential
+ @brief Message for @c FIRAuthErrorCodeInvalidCredential error code.
+ */
+static NSString *const kFIRAuthErrorMessageInvalidCredential = @"The supplied auth credential is "
+ "malformed or has expired.";
+
+/** @var kFIRAuthErrorMessageUserDisabled
+ @brief Message for @c FIRAuthErrorCodeUserDisabled error code.
+ */
+static NSString *const kFIRAuthErrorMessageUserDisabled = @"The user account has been disabled by "
+ "an administrator.";
+
+/** @var kFIRAuthErrorMessageEmailAlreadyInUse
+ @brief Message for @c FIRAuthErrorCodeEmailAlreadyInUse error code.
+ */
+static NSString *const kFIRAuthErrorMessageEmailAlreadyInUse = @"The email address is already in "
+ "use by another account.";
+
+/** @var kFIRAuthErrorMessageWrongPassword
+ @brief Message for @c FIRAuthErrorCodeWrongPassword error code.
+ */
+static NSString *const kFIRAuthErrorMessageWrongPassword = @"The password is invalid or the user "
+ "does not have a password.";
+
+/** @var kFIRAuthErrorMessageTooManyRequests
+ @brief Message for @c FIRAuthErrorCodeTooManyRequests error code.
+ */
+static NSString *const kFIRAuthErrorMessageTooManyRequests = @"We have blocked all requests from "
+ "this device due to unusual activity. Try again later.";
+
+/** @var kFIRAuthErrorMessageAccountExistsWithDifferentCredential
+ @brief Message for @c FIRAuthErrorCodeAccountLinkNeeded error code.
+ */
+static NSString *const kFIRAuthErrorMessageAccountExistsWithDifferentCredential = @"An account "
+ "already exists with the same email address but different sign-in credentials. Sign in using a "
+ "provider associated with this email address.";
+
+/** @var kFIRAuthErrorMessageRequiresRecentLogin
+ @brief Message for @c FIRAuthErrorCodeRequiresRecentLogin error code.
+ */
+static NSString *const kFIRAuthErrorMessageRequiresRecentLogin= @"This operation is sensitive and "
+ "requires recent authentication. Log in again before retrying this request.";
+
+/** @var kFIRAuthErrorMessageProviderAlreadyLinked
+ @brief Message for @c FIRAuthErrorCodeProviderAlreadyExists error code.
+ */
+static NSString *const kFIRAuthErrorMessageProviderAlreadyLinked =
+ @"[ERROR_PROVIDER_ALREADY_LINKED] - User can only be linked to one identity for the given "
+ "provider.";
+
+/** @var kFIRAuthErrorMessageNoSuchProvider
+ @brief Message for @c FIRAuthErrorCodeNoSuchProvider error code.
+ */
+static NSString *const kFIRAuthErrorMessageNoSuchProvider = @"User was not linked to an account "
+ "with the given provider.";
+
+/** @var kFIRAuthErrorMessageInvalidUserToken
+ @brief Message for @c FIRAuthErrorCodeInvalidUserToken error code.
+ */
+static NSString *const kFIRAuthErrorMessageInvalidUserToken = @"The user's credential is no longer "
+ "valid. The user must sign in again.";
+
+/** @var kFIRAuthErrorMessageNetworkError
+ @brief Message for @c FIRAuthErrorCodeNetworkError error code.
+ */
+static NSString *const kFIRAuthErrorMessageNetworkError = @"Network error (such as timeout, "
+ "interrupted connection or unreachable host) has occurred.";
+
+/** @var kFIRAuthErrorMessageKeychainError
+ @brief Message for @c FIRAuthErrorCodeKeychainError error code.
+ */
+static NSString *const kFIRAuthErrorMessageKeychainError = @"An error occurred when accessing the "
+ "keychain. The @c NSLocalizedFailureReasonErrorKey field in the @c NSError.userInfo dictionary "
+ "will contain more information about the error encountered";
+
+/** @var kFIRAuthErrorMessageUserTokenExpired
+ @brief Message for @c FIRAuthErrorCodeTokenExpired error code.
+ */
+static NSString *const kFIRAuthErrorMessageUserTokenExpired = @"The user's credential is no longer "
+ "valid. The user must sign in again.";
+
+/** @var kFIRAuthErrorMessageUserNotFound
+ @brief Message for @c FIRAuthErrorCodeUserNotFound error code.
+ */
+static NSString *const kFIRAuthErrorMessageUserNotFound = @"There is no user record corresponding "
+ "to this identifier. The user may have been deleted.";
+
+/** @var kFIRAuthErrorMessageInvalidAPIKey
+ @brief Message for @c FIRAuthErrorCodeInvalidAPIKey error code.
+ @remarks This error is not thrown by the server.
+ */
+static NSString *const kFIRAuthErrorMessageInvalidAPIKey = @"An invalid API Key was supplied in "
+ "the request.";
+
+/** @var kFIRAuthErrorMessageUserMismatch.
+ @brief Message for @c FIRAuthErrorCodeInvalidAPIKey error code.
+ */
+static NSString *const FIRAuthErrorMessageUserMismatch = @"The supplied credentials do not "
+ "correspond to the previously signed in user.";
+
+/** @var kFIRAuthErrorMessageCredentialAlreadyInUse
+ @brief Message for @c FIRAuthErrorCodeCredentialAlreadyInUse error code.
+ */
+static NSString *const kFIRAuthErrorMessageCredentialAlreadyInUse = @"This credential is already "
+ "associated with a different user account.";
+
+/** @var kFIRAuthErrorMessageOperationNotAllowed
+ @brief Message for @c FIRAuthErrorCodeOperationNotAllowed error code.
+ */
+static NSString *const kFIRAuthErrorMessageOperationNotAllowed = @"The given sign-in provider is "
+ "disabled for this Firebase project. Enable it in the Firebase console, under the sign-in "
+ "method tab of the Auth section.";
+
+/** @var kFIRAuthErrorMessageWeakPassword
+ @brief Message for @c FIRAuthErrorCodeWeakPassword error code.
+ */
+static NSString *const kFIRAuthErrorMessageWeakPassword = @"The password must be 6 characters long "
+ "or more.";
+
+/** @var kFIRAuthErrorMessageAppNotAuthorized
+ @brief Message for @c FIRAuthErrorCodeAppNotAuthorized error code.
+ */
+static NSString *const kFIRAuthErrorMessageAppNotAuthorized = @"This app is not authorized to use "
+ "Firebase Authentication with the provided API key. Review your key configuration in the "
+ "Google API console and ensure that it accepts requests from your app's bundle ID.";
+
+/** @var kFIRAuthErrorMessageExpiredActionCode
+ @brief Message for @c FIRAuthErrorCodeExpiredActionCode error code.
+ */
+static NSString *const kFIRAuthErrorMessageExpiredActionCode = @"The action code has expired.";
+
+/** @var kFIRAuthErrorMessageInvalidActionCode
+ @brief Message for @c FIRAuthErrorCodeInvalidActionCode error code.
+ */
+static NSString *const kFIRAuthErrorMessageInvalidActionCode = @"The action code is invalid. This "
+ "can happen if the code is malformed, expired, or has already been used.";
+
+/** @var kFIRAuthErrorMessageInvalidMessagePayload
+ @brief Message for @c FIRAuthErrorCodeInvalidMessagePayload error code.
+ */
+static NSString *const kFIRAuthErrorMessageInvalidMessagePayload = @"The action code is invalid. "
+ "This can happen if the code is malformed, expired, or has already been used.";
+
+/** @var kFIRAuthErrorMessageInvalidSender
+ @brief Message for @c FIRAuthErrorCodeInvalidSender error code.
+ */
+static NSString *const kFIRAuthErrorMessageInvalidSender = @"The email template corresponding to "
+ "this action contains invalid characters in its message. Please fix by going to the Auth email "
+ "templates section in the Firebase Console.";
+
+/** @var kFIRAuthErrorMessageInvalidRecipientEmail
+ @brief Message for @c FIRAuthErrorCodeInvalidRecipient error code.
+ */
+static NSString *const kFIRAuthErrorMessageInvalidRecipientEmail = @"The action code is invalid. "
+ "This can happen if the code is malformed, expired, or has already been used.";
+
+/** @var kFIRAuthErrorMessageMissingContinueURI
+ @brief Message for @c FIRAuthErrorCodeMissingContinueURI error code.
+ */
+static NSString *const kFIRAuthErrorMessageMissingContinueURI =
+ @"A continue URL must be provided in the request.";
+
+/** @var kFIRAuthErrorMessageMissingPhoneNumber
+ @brief Message for @c FIRAuthErrorCodeMissingPhoneNumber error code.
+ */
+static NSString *const kFIRAuthErrorMessageMissingPhoneNumber =
+ @"To send verification codes, provide a phone number for the recipient.";
+
+/** @var kFIRAuthErrorMessageInvalidPhoneNumber
+ @brief Message for @c FIRAuthErrorCodeMissingPhoneNumber error code.
+ */
+static NSString *const kFIRAuthErrorMessageInvalidPhoneNumber =
+ @"The format of the phone number provided is incorrect. Please enter the phone number in a "
+ "format that can be parsed into E.164 format. E.164 phone numbers are written in the format "
+ "[+][country code][subscriber number including area code].";
+
+/** @var kFIRAuthErrorMessageMissingVerificationCode
+ @brief Message for @c FIRAuthErrorCodeMissingVerificationCode error code.
+ */
+static NSString *const kFIRAuthErrorMessageMissingVerificationCode =
+ @"The Phone Auth Credential was created with an empty SMS verification Code.";
+
+/** @var kFIRAuthErrorMessageInvalidVerificationCode
+ @brief Message for @c FIRAuthErrorCodeInvalidVerificationCode error code.
+ */
+static NSString *const kFIRAuthErrorMessageInvalidVerificationCode =
+ @"The SMS verification code used to create the phone auth credential is invalid. Please resend "
+ "the verification code sms and be sure use the verification code provided by the user.";
+
+/** @var kFIRAuthErrorMessageMissingVerificationID
+ @brief Message for @c FIRAuthErrorCodeInvalidVerificationID error code.
+ */
+static NSString *const kFIRAuthErrorMessageMissingVerificationID =
+ @"The Phone Auth Credential was created with an empty verification ID.";
+
+/** @var kFIRAuthErrorMessageInvalidVerificationID
+ @brief Message for @c FIRAuthErrorCodeInvalidVerificationID error code.
+ */
+static NSString *const kFIRAuthErrorMessageInvalidVerificationID =
+ @"The verification ID used to create the phone auth credential is invalid.";
+
+/** @var kFIRAuthErrorMessageSessionExpired
+ @brief Message for @c FIRAuthErrorCodeSessionExpired error code.
+ */
+static NSString *const kFIRAuthErrorMessageSessionExpired = @"The SMS code has expired. Please "
+ @"re-send the verification code to try again.";
+
+/** @var kFIRAuthErrorMessageMissingAppCredential
+ @brief Message for @c FIRAuthErrorCodeMissingAppCredential error code.
+ */
+static NSString *const kFIRAuthErrorMessageMissingAppCredential = @"The phone verification request "
+ "is missing an APNs Device token. Firebase Auth automatically detects APNs Device Tokens, "
+ "however, if method swizzling is disabled, the APNs token must be set via the APNSToken "
+ "property on FIRAuth or by calling setAPNSToken:type on FIRAuth.";
+
+/** @var kFIRAuthErrorMessageInvalidAppCredential
+ @brief Message for @c FIRAuthErrorCodeInvalidAppCredential error code.
+ */
+static NSString *const kFIRAuthErrorMessageInvalidAppCredential = @"The APNs device token provided "
+ "may be incorrect or does not match the private certificate uploaded to the Firebase Console.";
+
+/** @var kFIRAuthErrorMessageQuotaExceeded
+ @brief Message for @c FIRAuthErrorCodeQuotaExceeded error code.
+ */
+static NSString *const kFIRAuthErrorMessageQuotaExceeded = @"The SMS quota for this project has "
+ "been exceeded.";
+
+/** @var kFIRAuthErrorMessageMissingAppToken
+ @brief Message for @c FIRAuthErrorCodeMissingAppToken error code.
+ */
+static NSString *const kFIRAuthErrorMessageMissingAppToken = @"Remote notification and background "
+ "fetching need to be set up for the app. If app delegate swizzling is disabled, the APNs "
+ "device token received by UIApplicationDelegate needs to be forwarded to FIRAuth's APNSToken "
+ "property.";
+
+/** @var kFIRAuthErrorMessageMissingAppToken
+ @brief Message for @c FIRAuthErrorCodeMissingAppToken error code.
+ */
+static NSString *const kFIRAuthErrorMessageNotificationNotForwarded = @"If app delegate swizzling "
+ "is disabled, remote notifications received by UIApplicationDelegate need to be forwarded to "
+ "FIRAuth's canHandleNotificaton: method.";
+
+/** @var kFIRAuthErrorMessageAppNotVerified
+ @brief Message for @c FIRAuthErrorCodeMissingAppToken error code.
+ */
+static NSString *const kFIRAuthErrorMessageAppNotVerified = @"Firebase could not retrieve the "
+ "silent push notification and therefore could not verify your app. Ensure that you configured "
+ "your app correctly to recieve push notifications.";
+
+/** @var kFIRAuthErrorMessageInternalError
+ @brief Message for @c FIRAuthErrorCodeInternalError error code.
+ */
+static NSString *const kFIRAuthErrorMessageInternalError = @"An internal error has occurred, "
+ "print and inspect the error details for more information.";
+
+/** @var FIRAuthErrorDescription
+ @brief The error descrioption, based on the error code.
+ @remarks No default case so that we get a compiler warning if a new value was added to the enum.
+ */
+static NSString *FIRAuthErrorDescription(FIRAuthErrorCode code) {
+ switch (code) {
+ case FIRAuthErrorCodeInvalidCustomToken:
+ return kFIRAuthErrorMessageInvalidCustomToken;
+ case FIRAuthErrorCodeCustomTokenMismatch:
+ return kFIRAuthErrorMessageCustomTokenMismatch;
+ case FIRAuthErrorCodeInvalidEmail:
+ return kFIRAuthErrorMessageInvalidEmail;
+ case FIRAuthErrorCodeInvalidCredential:
+ return kFIRAuthErrorMessageInvalidCredential;
+ case FIRAuthErrorCodeUserDisabled:
+ return kFIRAuthErrorMessageUserDisabled;
+ case FIRAuthErrorCodeEmailAlreadyInUse:
+ return kFIRAuthErrorMessageEmailAlreadyInUse;
+ case FIRAuthErrorCodeWrongPassword:
+ return kFIRAuthErrorMessageWrongPassword;
+ case FIRAuthErrorCodeTooManyRequests:
+ return kFIRAuthErrorMessageTooManyRequests;
+ case FIRAuthErrorCodeAccountExistsWithDifferentCredential:
+ return kFIRAuthErrorMessageAccountExistsWithDifferentCredential;
+ case FIRAuthErrorCodeRequiresRecentLogin:
+ return kFIRAuthErrorMessageRequiresRecentLogin;
+ case FIRAuthErrorCodeProviderAlreadyLinked:
+ return kFIRAuthErrorMessageProviderAlreadyLinked;
+ case FIRAuthErrorCodeNoSuchProvider:
+ return kFIRAuthErrorMessageNoSuchProvider;
+ case FIRAuthErrorCodeInvalidUserToken:
+ return kFIRAuthErrorMessageInvalidUserToken;
+ case FIRAuthErrorCodeNetworkError:
+ return kFIRAuthErrorMessageNetworkError;
+ case FIRAuthErrorCodeKeychainError:
+ return kFIRAuthErrorMessageKeychainError;
+ case FIRAuthErrorCodeUserTokenExpired:
+ return kFIRAuthErrorMessageUserTokenExpired;
+ case FIRAuthErrorCodeUserNotFound:
+ return kFIRAuthErrorMessageUserNotFound;
+ case FIRAuthErrorCodeInvalidAPIKey:
+ return kFIRAuthErrorMessageInvalidAPIKey;
+ case FIRAuthErrorCodeCredentialAlreadyInUse:
+ return kFIRAuthErrorMessageCredentialAlreadyInUse;
+ case FIRAuthErrorCodeInternalError:
+ return kFIRAuthErrorMessageInternalError;
+ case FIRAuthErrorCodeUserMismatch:
+ return FIRAuthErrorMessageUserMismatch;
+ case FIRAuthErrorCodeOperationNotAllowed:
+ return kFIRAuthErrorMessageOperationNotAllowed;
+ case FIRAuthErrorCodeWeakPassword:
+ return kFIRAuthErrorMessageWeakPassword;
+ case FIRAuthErrorCodeAppNotAuthorized:
+ return kFIRAuthErrorMessageAppNotAuthorized;
+ case FIRAuthErrorCodeExpiredActionCode:
+ return kFIRAuthErrorMessageExpiredActionCode;
+ case FIRAuthErrorCodeInvalidActionCode:
+ return kFIRAuthErrorMessageInvalidActionCode;
+ case FIRAuthErrorCodeInvalidSender:
+ return kFIRAuthErrorMessageInvalidSender;
+ case FIRAuthErrorCodeInvalidMessagePayload:
+ return kFIRAuthErrorMessageInvalidMessagePayload;
+ case FIRAuthErrorCodeInvalidRecipientEmail:
+ return kFIRAuthErrorMessageInvalidRecipientEmail;
+ case FIRAuthErrorCodeMissingPhoneNumber:
+ return kFIRAuthErrorMessageMissingPhoneNumber;
+ case FIRAuthErrorCodeInvalidPhoneNumber:
+ return kFIRAuthErrorMessageInvalidPhoneNumber;
+ case FIRAuthErrorCodeMissingVerificationCode:
+ return kFIRAuthErrorMessageMissingVerificationCode;
+ case FIRAuthErrorCodeInvalidVerificationCode:
+ return kFIRAuthErrorMessageInvalidVerificationCode;
+ case FIRAuthErrorCodeMissingVerificationID:
+ return kFIRAuthErrorMessageMissingVerificationID;
+ case FIRAuthErrorCodeInvalidVerificationID:
+ return kFIRAuthErrorMessageInvalidVerificationID;
+ case FIRAuthErrorCodeSessionExpired:
+ return kFIRAuthErrorMessageSessionExpired;
+ case FIRAuthErrorCodeMissingAppCredential:
+ return kFIRAuthErrorMessageMissingAppCredential;
+ case FIRAuthErrorCodeInvalidAppCredential:
+ return kFIRAuthErrorMessageInvalidAppCredential;
+ case FIRAuthErrorCodeQuotaExceeded:
+ return kFIRAuthErrorMessageQuotaExceeded;
+ case FIRAuthErrorCodeMissingAppToken:
+ return kFIRAuthErrorMessageMissingAppToken;
+ case FIRAuthErrorCodeNotificationNotForwarded:
+ return kFIRAuthErrorMessageNotificationNotForwarded;
+ case FIRAuthErrorCodeAppNotVerified:
+ return kFIRAuthErrorMessageAppNotVerified;
+ }
+}
+
+/** @var FIRAuthErrorCodeString
+ @brief The the error short string, based on the error code.
+ @remarks No default case so that we get a compiler warning if a new value was added to the enum.
+ */
+static NSString *const FIRAuthErrorCodeString(FIRAuthErrorCode code) {
+ switch (code) {
+ case FIRAuthErrorCodeInvalidCustomToken:
+ return @"ERROR_INVALID_CUSTOM_TOKEN";
+ case FIRAuthErrorCodeCustomTokenMismatch:
+ return @"ERROR_CUSTOM_TOKEN_MISMATCH";
+ case FIRAuthErrorCodeInvalidEmail:
+ return @"ERROR_INVALID_EMAIL";
+ case FIRAuthErrorCodeInvalidCredential:
+ return @"ERROR_INVALID_CREDENTIAL";
+ case FIRAuthErrorCodeUserDisabled:
+ return @"ERROR_USER_DISABLED";
+ case FIRAuthErrorCodeEmailAlreadyInUse:
+ return @"ERROR_EMAIL_ALREADY_IN_USE";
+ case FIRAuthErrorCodeWrongPassword:
+ return @"ERROR_WRONG_PASSWORD";
+ case FIRAuthErrorCodeTooManyRequests:
+ return @"ERROR_TOO_MANY_REQUESTS";
+ case FIRAuthErrorCodeAccountExistsWithDifferentCredential:
+ return @"ERROR_ACCOUNT_EXISTS_WITH_DIFFERENT_CREDENTIAL";
+ case FIRAuthErrorCodeRequiresRecentLogin:
+ return @"ERROR_REQUIRES_RECENT_LOGIN";
+ case FIRAuthErrorCodeProviderAlreadyLinked:
+ return @"ERROR_PROVIDER_ALREADY_LINKED";
+ case FIRAuthErrorCodeNoSuchProvider:
+ return @"ERROR_NO_SUCH_PROVIDER";
+ case FIRAuthErrorCodeInvalidUserToken:
+ return @"ERROR_INVALID_USER_TOKEN";
+ case FIRAuthErrorCodeNetworkError:
+ return @"ERROR_NETWORK_REQUEST_FAILED";
+ case FIRAuthErrorCodeKeychainError:
+ return @"ERROR_KEYCHAIN_ERROR";
+ case FIRAuthErrorCodeUserTokenExpired:
+ return @"ERROR_USER_TOKEN_EXPIRED";
+ case FIRAuthErrorCodeUserNotFound:
+ return @"ERROR_USER_NOT_FOUND";
+ case FIRAuthErrorCodeInvalidAPIKey:
+ return @"ERROR_INVALID_API_KEY";
+ case FIRAuthErrorCodeCredentialAlreadyInUse:
+ return @"ERROR_CREDENTIAL_ALREADY_IN_USE";
+ case FIRAuthErrorCodeInternalError:
+ return @"ERROR_INTERNAL_ERROR";
+ case FIRAuthErrorCodeUserMismatch:
+ return @"ERROR_USER_MISMATCH";
+ case FIRAuthErrorCodeOperationNotAllowed:
+ return @"ERROR_OPERATION_NOT_ALLOWED";
+ case FIRAuthErrorCodeWeakPassword:
+ return @"ERROR_WEAK_PASSWORD";
+ case FIRAuthErrorCodeAppNotAuthorized:
+ return @"ERROR_APP_NOT_AUTHORIZED";
+ case FIRAuthErrorCodeExpiredActionCode:
+ return @"ERROR_EXPIRED_ACTION_CODE";
+ case FIRAuthErrorCodeInvalidActionCode:
+ return @"ERROR_INVALID_ACTION_CODE";
+ case FIRAuthErrorCodeInvalidMessagePayload:
+ return @"ERROR_INVALID_MESSAGE_PAYLOAD";
+ case FIRAuthErrorCodeInvalidSender:
+ return @"ERROR_INVALID_SENDER";
+ case FIRAuthErrorCodeInvalidRecipientEmail:
+ return @"ERROR_INVALID_RECIPIENT_EMAIL";
+ case FIRAuthErrorCodeMissingPhoneNumber:
+ return @"ERROR_MISSING_PHONE_NUMBER";
+ case FIRAuthErrorCodeInvalidPhoneNumber:
+ return @"ERROR_INVALID_PHONE_NUMBER";
+ case FIRAuthErrorCodeMissingVerificationCode:
+ return @"ERROR_MISSING_VERIFICATION_CODE";
+ case FIRAuthErrorCodeInvalidVerificationCode:
+ return @"ERROR_INVALID_VERIFICATION_CODE";
+ case FIRAuthErrorCodeMissingVerificationID:
+ return @"ERROR_MISSING_VERIFICATION_ID";
+ case FIRAuthErrorCodeInvalidVerificationID:
+ return @"ERROR_INVALID_VERIFICATION_ID";
+ case FIRAuthErrorCodeSessionExpired:
+ return @"ERROR_SESSION_EXPIRED";
+ case FIRAuthErrorCodeMissingAppCredential:
+ return @"MISSING_APP_CREDENTIAL";
+ case FIRAuthErrorCodeInvalidAppCredential:
+ return @"INVALID_APP_CREDENTIAL";
+ case FIRAuthErrorCodeQuotaExceeded:
+ return @"ERROR_QUOTA_EXCEEDED";
+ case FIRAuthErrorCodeMissingAppToken:
+ return @"ERROR_MISSING_APP_TOKEN";
+ case FIRAuthErrorCodeNotificationNotForwarded:
+ return @"ERROR_NOTIFICATION_NOT_FORWARDED";
+ case FIRAuthErrorCodeAppNotVerified:
+ return @"ERROR_APP_NOT_VERIFIED";
+ }
+}
+
+@implementation FIRAuthErrorUtils
+
++ (NSError *)errorWithCode:(FIRAuthInternalErrorCode)code {
+ return [self errorWithCode:code message:nil];
+}
+
++ (NSError *)errorWithCode:(FIRAuthInternalErrorCode)code
+ message:(nullable NSString *)message {
+ NSDictionary *userInfo = nil;
+ if (message.length) {
+ userInfo = @{
+ NSLocalizedDescriptionKey : message
+ };
+ }
+ return [self errorWithCode:code userInfo:userInfo];
+}
+
++ (NSError *)errorWithCode:(FIRAuthInternalErrorCode)code
+ underlyingError:(nullable NSError *)underlyingError {
+ NSDictionary *errorUserInfo = nil;
+ if (underlyingError) {
+ errorUserInfo = @{
+ NSUnderlyingErrorKey : underlyingError
+ };
+ }
+ return [self errorWithCode:code userInfo:errorUserInfo];
+}
+
++ (NSError *)errorWithCode:(FIRAuthInternalErrorCode)code userInfo:(NSDictionary *)userInfo {
+ BOOL isPublic = (code & FIRAuthPublicErrorCodeFlag) == FIRAuthPublicErrorCodeFlag;
+ if (isPublic) {
+ // This is a public error. Return it as a public error and add a description.
+ NSInteger errorCode = code & ~FIRAuthPublicErrorCodeFlag;
+ NSMutableDictionary *errorUserInfo = [NSMutableDictionary dictionaryWithDictionary:userInfo];
+ if (!errorUserInfo[NSLocalizedDescriptionKey]) {
+ errorUserInfo[NSLocalizedDescriptionKey] = FIRAuthErrorDescription(errorCode);
+ }
+ errorUserInfo[FIRAuthErrorNameKey] = FIRAuthErrorCodeString(errorCode);
+ return [NSError errorWithDomain:FIRAuthErrorDomain code:errorCode userInfo:errorUserInfo];
+ } else {
+ // This is an internal error. Wrap it in an internal error.
+ NSError *error =
+ [NSError errorWithDomain:FIRAuthInternalErrorDomain code:code userInfo:userInfo];
+ return [self errorWithCode:FIRAuthInternalErrorCodeInternalError underlyingError:error];
+ }
+}
+
++ (NSError *)RPCRequestEncodingErrorWithUnderlyingError:(NSError *)underlyingError {
+ return [self errorWithCode:FIRAuthInternalErrorCodeRPCRequestEncodingError
+ underlyingError:underlyingError];
+}
+
++ (NSError *)JSONSerializationErrorForUnencodableType {
+ return [self errorWithCode:FIRAuthInternalErrorCodeJSONSerializationError];
+}
+
++ (NSError *)JSONSerializationErrorWithUnderlyingError:(NSError *)underlyingError {
+ return [self errorWithCode:FIRAuthInternalErrorCodeJSONSerializationError
+ underlyingError:underlyingError];
+}
+
++ (NSError *)networkErrorWithUnderlyingError:(NSError *)underlyingError {
+ return [self errorWithCode:FIRAuthInternalErrorCodeNetworkError
+ underlyingError:underlyingError];
+}
+
++ (NSError *)unexpectedErrorResponseWithData:(NSData *)data
+ underlyingError:(NSError *)underlyingError {
+ return [self errorWithCode:FIRAuthInternalErrorCodeUnexpectedErrorResponse userInfo:@{
+ FIRAuthErrorUserInfoDataKey : data,
+ NSUnderlyingErrorKey : underlyingError
+ }];
+}
+
++ (NSError *)unexpectedErrorResponseWithDeserializedResponse:(id)deserializedResponse {
+ return [self errorWithCode:FIRAuthInternalErrorCodeUnexpectedErrorResponse userInfo:@{
+ FIRAuthErrorUserInfoDeserializedResponseKey : deserializedResponse
+ }];
+}
+
++ (NSError *)unexpectedResponseWithData:(NSData *)data
+ underlyingError:(NSError *)underlyingError {
+ return [self errorWithCode:FIRAuthInternalErrorCodeUnexpectedResponse userInfo:@{
+ FIRAuthErrorUserInfoDataKey : data,
+ NSUnderlyingErrorKey : underlyingError
+ }];
+}
+
++ (NSError *)unexpectedResponseWithDeserializedResponse:(id)deserializedResponse {
+ return [self errorWithCode:FIRAuthInternalErrorCodeUnexpectedResponse userInfo:@{
+ FIRAuthErrorUserInfoDeserializedResponseKey : deserializedResponse
+ }];
+}
+
++ (NSError *)unexpectedResponseWithDeserializedResponse:(nullable id)deserializedResponse
+ underlyingError:(NSError *)underlyingError {
+ NSMutableDictionary *userInfo =
+ [NSMutableDictionary dictionaryWithDictionary:@{ NSUnderlyingErrorKey : underlyingError }];
+ if (deserializedResponse) {
+ userInfo[FIRAuthErrorUserInfoDeserializedResponseKey] = deserializedResponse;
+ }
+ return [self errorWithCode:FIRAuthInternalErrorCodeUnexpectedResponse userInfo:userInfo];
+}
+
++ (NSError *)RPCResponseDecodingErrorWithDeserializedResponse:(id)deserializedResponse
+ underlyingError:(NSError *)underlyingError {
+ return [self errorWithCode:FIRAuthInternalErrorCodeRPCResponseDecodingError userInfo:@{
+ FIRAuthErrorUserInfoDeserializedResponseKey : deserializedResponse,
+ NSUnderlyingErrorKey : underlyingError
+ }];
+}
+
++ (NSError *)emailAlreadyInUseErrorWithEmail:(nullable NSString *)email {
+ NSMutableDictionary *userInfo = [[NSMutableDictionary alloc] init];
+ if (email.length) {
+ userInfo[FIRAuthErrorUserInfoEmailKey] = email;
+ }
+ return [self errorWithCode:FIRAuthInternalErrorCodeEmailAlreadyInUse userInfo:userInfo];
+}
+
++ (NSError *)userDisabledErrorWithMessage:(nullable NSString *)message {
+ return [self errorWithCode:FIRAuthInternalErrorCodeUserDisabled message:message];
+}
+
++ (NSError *)wrongPasswordErrorWithMessage:(nullable NSString *)message {
+ return [self errorWithCode:FIRAuthInternalErrorCodeWrongPassword message:message];
+}
+
++ (NSError *)tooManyRequestsErrorWithMessage:(nullable NSString *)message {
+ return [self errorWithCode:FIRAuthInternalErrorCodeTooManyRequests message:message];
+}
+
++ (NSError *)invalidCustomTokenErrorWithMessage:(nullable NSString *)message {
+ return [self errorWithCode:FIRAuthInternalErrorCodeInvalidCustomToken message:message];
+}
+
++ (NSError *)customTokenMistmatchErrorWithMessage:(nullable NSString *)message {
+ return [self errorWithCode:FIRAuthInternalErrorCodeCustomTokenMismatch message:message];
+}
+
++ (NSError *)invalidCredentialErrorWithMessage:(nullable NSString *)message {
+ return [self errorWithCode:FIRAuthInternalErrorCodeInvalidCredential message:message];
+}
+
++ (NSError *)requiresRecentLoginErrorWithMessage:(nullable NSString *)message {
+ return [self errorWithCode:FIRAuthInternalErrorCodeRequiresRecentLogin message:message];
+}
+
++ (NSError *)invalidUserTokenErrorWithMessage:(nullable NSString *)message {
+ return [self errorWithCode:FIRAuthInternalErrorCodeInvalidUserToken message:message];
+}
+
++ (NSError *)invalidEmailErrorWithMessage:(nullable NSString *)message {
+ return [self errorWithCode:FIRAuthInternalErrorCodeInvalidEmail message:message];
+}
+
++ (NSError *)accountExistsWithDifferentCredentialErrorWithEmail:(nullable NSString *)email {
+ return [self errorWithCode:FIRAuthInternalErrorCodeAccountExistsWithDifferentCredential
+ userInfo:@{ FIRAuthErrorUserInfoEmailKey : email }];
+}
+
++ (NSError *)providerAlreadyLinkedError {
+ return [self errorWithCode:FIRAuthInternalErrorCodeProviderAlreadyLinked];
+}
+
++ (NSError *)noSuchProviderError {
+ return [self errorWithCode:FIRAuthInternalErrorCodeNoSuchProvider];
+}
+
++ (NSError *)userTokenExpiredErrorWithMessage:(nullable NSString *)message {
+ return [self errorWithCode:FIRAuthInternalErrorCodeUserTokenExpired message:message];
+}
+
++ (NSError *)userNotFoundErrorWithMessage:(nullable NSString *)message {
+ return [self errorWithCode:FIRAuthInternalErrorCodeUserNotFound message:message];
+}
+
++ (NSError *)invalidAPIKeyError {
+ return [self errorWithCode:FIRAuthInternalErrorCodeInvalidAPIKey];
+}
+
++ (NSError *)userMismatchError {
+ return [self errorWithCode:FIRAuthInternalErrorCodeUserMismatch];
+}
+
++ (NSError *)credentialAlreadyInUseErrorWithMessage:(nullable NSString *)message
+ credential:(nullable FIRPhoneAuthCredential *)credential {
+ if (credential) {
+ return [self errorWithCode:FIRAuthInternalErrorCodeCredentialAlreadyInUse
+ userInfo:@{ FIRAuthUpdatedCredentialKey : credential }];
+ }
+ return [self errorWithCode:FIRAuthInternalErrorCodeCredentialAlreadyInUse message:message];
+}
+
++ (NSError *)operationNotAllowedErrorWithMessage:(nullable NSString *)message {
+ return [self errorWithCode:FIRAuthInternalErrorCodeOperationNotAllowed message:message];
+}
+
++ (NSError *)weakPasswordErrorWithServerResponseReason:(NSString *)reason {
+ return [self errorWithCode:FIRAuthInternalErrorCodeWeakPassword userInfo:@{
+ NSLocalizedFailureReasonErrorKey : reason
+ }];
+}
+
++ (NSError *)appNotAuthorizedError {
+ return [self errorWithCode:FIRAuthInternalErrorCodeAppNotAuthorized];
+}
+
++ (NSError *)expiredActionCodeErrorWithMessage:(nullable NSString *)message {
+ return [self errorWithCode:FIRAuthInternalErrorCodeExpiredActionCode message:message];
+}
+
++ (NSError *)invalidActionCodeErrorWithMessage:(nullable NSString *)message {
+ return [self errorWithCode:FIRAuthInternalErrorCodeInvalidActionCode message:message];
+}
+
++ (NSError *)invalidMessagePayloadErrorWithMessage:(nullable NSString *)message {
+ return [self errorWithCode:FIRAuthInternalErrorCodeInvalidMessagePayload message:message];
+}
+
++ (NSError *)invalidSenderErrorWithMessage:(nullable NSString *)message {
+ return [self errorWithCode:FIRAuthInternalErrorCodeInvalidSender message:message];
+}
+
++ (NSError *)invalidRecipientEmailErrorWithMessage:(nullable NSString *)message {
+ return [self errorWithCode:FIRAuthInternalErrorCodeInvalidRecipientEmail message:message];
+}
+
++ (NSError *)missingPhoneNumberErrorWithMessage:(nullable NSString *)message {
+ return [self errorWithCode:FIRAuthInternalErrorCodeMissingPhoneNumber message:message];
+}
+
++ (NSError *)invalidPhoneNumberErrorWithMessage:(nullable NSString *)message {
+ return [self errorWithCode:FIRAuthInternalErrorCodeInvalidPhoneNumber message:message];
+}
+
++ (NSError *)missingVerificationCodeErrorWithMessage:(nullable NSString *)message {
+ return [self errorWithCode:FIRAuthInternalErrorCodeMissingVerificationCode message:message];
+}
+
++ (NSError *)invalidVerificationCodeErrorWithMessage:(nullable NSString *)message {
+ return [self errorWithCode:FIRAuthInternalErrorCodeInvalidVerificationCode message:message];
+}
+
++ (NSError *)missingVerificationIDErrorWithMessage:(nullable NSString *)message {
+ return [self errorWithCode:FIRAuthInternalErrorCodeMissingVerificationID message:message];
+}
+
++ (NSError *)invalidVerificationIDErrorWithMessage:(nullable NSString *)message {
+ return [self errorWithCode:FIRAuthInternalErrorCodeInvalidVerificationID message:message];
+}
+
++ (NSError *)sessionExpiredErrorWithMessage:(nullable NSString *)message {
+ return [self errorWithCode:FIRAuthInternalErrorCodeSessionExpired message:message];
+}
+
++ (NSError *)missingAppCredentialWithMessage:(nullable NSString *)message {
+ return [self errorWithCode:FIRAuthInternalErrorCodeMissingAppCredential message:message];
+}
+
++ (NSError *)invalidAppCredentialWithMessage:(nullable NSString *)message {
+ return [self errorWithCode:FIRAuthInternalErrorCodeInvalidAppCredential message:message];
+}
+
++ (NSError *)quotaExceededErrorWithMessage:(nullable NSString *)message {
+ return [self errorWithCode:FIRAuthInternalErrorCodeQuotaExceeded message:message];
+}
+
++ (NSError *)missingAppTokenError {
+ return [self errorWithCode:FIRAuthInternalErrorCodeMissingAppToken];
+}
+
++ (NSError *)notificationNotForwardedError {
+ return [self errorWithCode:FIRAuthInternalErrorCodeNotificationNotForwarded];
+}
+
++ (NSError *)appNotVerifiedErrorWithMessage:(nullable NSString *)message {
+ return [self errorWithCode:FIRAuthInternalErrorCodeAppNotVerified message:message];
+}
+
++ (NSError *)keychainErrorWithFunction:(NSString *)keychainFunction status:(OSStatus)status {
+ NSString *failureReason = [NSString stringWithFormat:@"%@ (%li)", keychainFunction, (long)status];
+ return [self errorWithCode:FIRAuthInternalErrorCodeKeychainError userInfo:@{
+ NSLocalizedFailureReasonErrorKey : failureReason,
+ }];
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/FIRAuthErrors.h b/Firebase/Auth/Source/FIRAuthErrors.h
new file mode 100644
index 0000000..c1e7900
--- /dev/null
+++ b/Firebase/Auth/Source/FIRAuthErrors.h
@@ -0,0 +1,258 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+#import "FIRAuthSwiftNameSupport.h"
+
+/** @class FIRAuthErrors
+ @remarks Error Codes common to all API Methods:
+ <ul>
+ <li>@c FIRAuthErrorCodeNetworkError</li>
+ <li>@c FIRAuthErrorCodeUserNotFound</li>
+ <li>@c FIRAuthErrorCodeUserTokenExpired</li>
+ <li>@c FIRAuthErrorCodeTooManyRequests</li>
+ <li>@c FIRAuthErrorCodeInvalidAPIKey</li>
+ <li>@c FIRAuthErrorCodeAppNotAuthorized</li>
+ <li>@c FIRAuthErrorCodeKeychainError</li>
+ <li>@c FIRAuthErrorCodeInternalError</li>
+ </ul>
+ @remarks Common error codes for @c FIRUser operations:
+ <ul>
+ <li>@c FIRAuthErrorCodeInvalidUserToken</li>
+ <li>@c FIRAuthErrorCodeUserDisabled</li>
+ </ul>
+ */
+FIR_SWIFT_NAME(AuthErrors)
+@interface FIRAuthErrors
+
+/**
+ @brief The Firebase Auth error domain.
+ */
+extern NSString *const FIRAuthErrorDomain FIR_SWIFT_NAME(AuthErrorDomain);
+
+/**
+ @brief The key used to read the updated credential from the userinfo dictionary of the NSError
+ object returned in the case that the credential being linked in already in use.
+ */
+extern NSString *const FIRAuthUpdatedCredentialKey FIR_SWIFT_NAME(AuthUpdatedCredentialKey);
+
+/**
+ @brief The name of the key for the "error_name" string in the NSError userinfo dictionary.
+ */
+extern NSString *const FIRAuthErrorNameKey FIR_SWIFT_NAME(AuthErrorNameKey);
+
+/** @var FIRAuthErrorUserInfoEmailKey
+ @brief Errors with the code @c FIRAuthErrorCodeEmailAlreadyInUse may contains an
+ @c NSError.userInfo dictinary which contains this key. The value associated with this key is
+ an NSString of the email address that already exists.
+ */
+extern NSString *const FIRAuthErrorUserInfoEmailKey FIR_SWIFT_NAME(AuthErrorUserInfoEmailKey);
+
+/**
+ @brief Error codes used by Firebase Auth.
+ */
+typedef NS_ENUM(NSInteger, FIRAuthErrorCode) {
+ /** Indicates a validation error with the custom token.
+ */
+ FIRAuthErrorCodeInvalidCustomToken = 17000,
+
+ /** Indicates the service account and the API key belong to different projects.
+ */
+ FIRAuthErrorCodeCustomTokenMismatch = 17002,
+
+ /** Indicates the IDP token or requestUri is invalid.
+ */
+ FIRAuthErrorCodeInvalidCredential = 17004,
+
+ /** Indicates the user's account is disabled on the server.
+ */
+ FIRAuthErrorCodeUserDisabled = 17005,
+
+ /** Indicates the administrator disabled sign in with the specified identity provider.
+ */
+ FIRAuthErrorCodeOperationNotAllowed = 17006,
+
+ /** Indicates the email used to attempt a sign up is already in use.
+ */
+ FIRAuthErrorCodeEmailAlreadyInUse = 17007,
+
+ /** Indicates the email is invalid.
+ */
+ FIRAuthErrorCodeInvalidEmail = 17008,
+
+ /** Indicates the user attempted sign in with a wrong password.
+ */
+ FIRAuthErrorCodeWrongPassword = 17009,
+
+ /** Indicates that too many requests were made to a server method.
+ */
+ FIRAuthErrorCodeTooManyRequests = 17010,
+
+ /** Indicates the user account was not found.
+ */
+ FIRAuthErrorCodeUserNotFound = 17011,
+
+ /** Indicates account linking is required.
+ */
+ FIRAuthErrorCodeAccountExistsWithDifferentCredential = 17012,
+
+ /** Indicates the user has attemped to change email or password more than 5 minutes after
+ signing in.
+ */
+ FIRAuthErrorCodeRequiresRecentLogin = 17014,
+
+ /** Indicates an attempt to link a provider to which the account is already linked.
+ */
+ FIRAuthErrorCodeProviderAlreadyLinked = 17015,
+
+ /** Indicates an attempt to unlink a provider that is not linked.
+ */
+ FIRAuthErrorCodeNoSuchProvider = 17016,
+
+ /** Indicates user's saved auth credential is invalid, the user needs to sign in again.
+ */
+ FIRAuthErrorCodeInvalidUserToken = 17017,
+
+ /** Indicates a network error occurred (such as a timeout, interrupted connection, or
+ unreachable host). These types of errors are often recoverable with a retry. The @c
+ NSUnderlyingError field in the @c NSError.userInfo dictionary will contain the error
+ encountered.
+ */
+ FIRAuthErrorCodeNetworkError = 17020,
+
+ /** Indicates the saved token has expired, for example, the user may have changed account
+ password on another device. The user needs to sign in again on the device that made this
+ request.
+ */
+ FIRAuthErrorCodeUserTokenExpired = 17021,
+
+ /** Indicates an invalid API key was supplied in the request.
+ */
+ FIRAuthErrorCodeInvalidAPIKey = 17023,
+
+ /** Indicates that an attempt was made to reauthenticate with a user which is not the current
+ user.
+ */
+ FIRAuthErrorCodeUserMismatch = 17024,
+
+ /** Indicates an attempt to link with a credential that has already been linked with a
+ different Firebase account
+ */
+ FIRAuthErrorCodeCredentialAlreadyInUse = 17025,
+
+ /** Indicates an attempt to set a password that is considered too weak.
+ */
+ FIRAuthErrorCodeWeakPassword = 17026,
+
+ /** Indicates the App is not authorized to use Firebase Authentication with the
+ provided API Key.
+ */
+ FIRAuthErrorCodeAppNotAuthorized = 17028,
+
+ /** Indicates the OOB code is expired.
+ */
+ FIRAuthErrorCodeExpiredActionCode = 17029,
+
+ /** Indicates the OOB code is invalid.
+ */
+ FIRAuthErrorCodeInvalidActionCode = 17030,
+
+ /** Indicates that there are invalid parameters in the payload during a "send password reset
+ * email" attempt.
+ */
+ FIRAuthErrorCodeInvalidMessagePayload = 17031,
+
+ /** Indicates that the sender email is invalid during a "send password reset email" attempt.
+ */
+ FIRAuthErrorCodeInvalidSender = 17032,
+
+ /** Indicates that the recipient email is invalid.
+ */
+ FIRAuthErrorCodeInvalidRecipientEmail = 17033,
+
+ // The enum values between 17033 and 17041 are reserved and should NOT be used for new error
+ // codes.
+
+ /** Indicates that a phone number was not provided in a call to @c
+ verifyPhoneNumber:completion:.
+ */
+ FIRAuthErrorCodeMissingPhoneNumber = 17041,
+
+ /** Indicates that an invalid phone number was provided in a call to @c
+ verifyPhoneNumber:completion:.
+ */
+ FIRAuthErrorCodeInvalidPhoneNumber = 17042,
+
+ /** Indicates that the phone auth credential was created with an empty verification code.
+ */
+ FIRAuthErrorCodeMissingVerificationCode = 17043,
+
+ /** Indicates that an invalid verification code was used in the verifyPhoneNumber request.
+ */
+ FIRAuthErrorCodeInvalidVerificationCode = 17044,
+
+ /** Indicates that the phone auth credential was created with an empty verification ID.
+ */
+ FIRAuthErrorCodeMissingVerificationID = 17045,
+
+ /** Indicates that an invalid verification ID was used in the verifyPhoneNumber request.
+ */
+ FIRAuthErrorCodeInvalidVerificationID = 17046,
+
+ /** Indicates that the APNS device token is missing in the verifyClient request.
+ */
+ FIRAuthErrorCodeMissingAppCredential = 17047,
+
+ /** Indicates that an invalid APNS device token was used in the verifyClient request.
+ */
+ FIRAuthErrorCodeInvalidAppCredential = 17048,
+
+ // The enum values between 17048 and 17051 are reserved and should NOT be used for new error
+ // codes.
+
+ /** Indicates that the SMS code has expired.
+ */
+ FIRAuthErrorCodeSessionExpired = 17051,
+
+ /** Indicates that the quota of SMS messages for a given project has been exceeded.
+ */
+ FIRAuthErrorCodeQuotaExceeded = 17052,
+
+ /** Indicates that the APNs device token could not be obtained. The app may not have set up
+ remote notification correctly, or may fail to forward the APNs device token to FIRAuth
+ if app delegate swizzling is disabled.
+ */
+ FIRAuthErrorCodeMissingAppToken = 17053,
+
+ /** Indicates that the app fails to forward remote notification to FIRAuth.
+ */
+ FIRAuthErrorCodeNotificationNotForwarded = 17054,
+
+ /** Indicates that the app could not be verified by Firebase during phone number authentication.
+ */
+ FIRAuthErrorCodeAppNotVerified = 17055,
+
+ /** Indicates an error occurred while attempting to access the keychain.
+ */
+ FIRAuthErrorCodeKeychainError = 17995,
+
+ /** Indicates an internal error occurred.
+ */
+ FIRAuthErrorCodeInternalError = 17999,
+} FIR_SWIFT_NAME(AuthErrorCode);
+
+@end
diff --git a/Firebase/Auth/Source/FIRAuthExceptionUtils.h b/Firebase/Auth/Source/FIRAuthExceptionUtils.h
new file mode 100644
index 0000000..3ae9159
--- /dev/null
+++ b/Firebase/Auth/Source/FIRAuthExceptionUtils.h
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @class FIRAuthExceptionUtils
+ @brief Utility class used to raise standardized Auth related exceptions.
+*/
+@interface FIRAuthExceptionUtils : NSObject
+
+/** @fn raiseInvalidParameterExceptionWithReason:
+ @brief raises the "invalid parameter" exception
+ @param reason string will contain a description of the error.
+ */
++ (void)raiseInvalidParameterExceptionWithReason:(nullable NSString *)reason;
+
+/** @fn raiseMethodNotImplementedExceptionWithReason:
+ @brief raises the "method not implemented" exception
+ @param reason string will contain a description of the error.
+ @see FIRMethodNotImplementedException
+ */
++ (void)raiseMethodNotImplementedExceptionWithReason:(nullable NSString *)reason;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/FIRAuthExceptionUtils.m b/Firebase/Auth/Source/FIRAuthExceptionUtils.m
new file mode 100644
index 0000000..0adcd34
--- /dev/null
+++ b/Firebase/Auth/Source/FIRAuthExceptionUtils.m
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRAuthExceptionUtils.h"
+
+/** @var FIRMethodNotImplementedException
+ @brief The name of the "Method Not Implemented" exception.
+ */
+static NSString *const FIRMethodNotImplementedException = @"FIRMethodNotImplementedException";
+
+@implementation FIRAuthExceptionUtils
+
++ (void)raiseInvalidParameterExceptionWithReason:(NSString *)reason {
+ [NSException raise:NSInvalidArgumentException format:@"%@", reason];
+}
+
++ (void)raiseMethodNotImplementedExceptionWithReason:(nullable NSString *)reason {
+ NSException *exception =
+ [NSException exceptionWithName:FIRMethodNotImplementedException reason:reason userInfo:nil];
+ [exception raise];
+}
+
+@end
diff --git a/Firebase/Auth/Source/FIRAuthGlobalWorkQueue.m b/Firebase/Auth/Source/FIRAuthGlobalWorkQueue.m
new file mode 100644
index 0000000..8780959
--- /dev/null
+++ b/Firebase/Auth/Source/FIRAuthGlobalWorkQueue.m
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "Private/FIRAuthGlobalWorkQueue.h"
+
+dispatch_queue_t FIRAuthGlobalWorkQueue() {
+ static dispatch_once_t once;
+ static dispatch_queue_t queue;
+ dispatch_once(&once, ^{
+ queue = dispatch_queue_create("com.google.firebase.auth.globalWorkQueue", NULL);
+ });
+ return queue;
+}
diff --git a/Firebase/Auth/Source/FIRAuthKeychain.m b/Firebase/Auth/Source/FIRAuthKeychain.m
new file mode 100644
index 0000000..68cf2f2
--- /dev/null
+++ b/Firebase/Auth/Source/FIRAuthKeychain.m
@@ -0,0 +1,256 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "Private/FIRAuthKeychain.h"
+
+#import <Security/Security.h>
+
+#import "Private/FIRAuthErrorUtils.h"
+#import "Private/FIRAuthUserDefaultsStorage.h"
+
+#if FIRAUTH_USER_DEFAULTS_STORAGE_AVAILABLE
+#import <UIKit/UIKit.h>
+
+/** @var kOSVersionMatcherForUsingUserDefaults
+ @brief The regular expression to match all OS versions that @c FIRAuthUserDefaultsStorage is
+ used instead if available.
+ */
+static NSString *const kOSVersionMatcherForUsingUserDefaults = @"^10\\.[01](\\..*)?$";
+
+#endif // FIRAUTH_USER_DEFAULTS_STORAGE_AVAILABLE
+
+/** @var kAccountPrefix
+ @brief The prefix string for keychain item account attribute before the key.
+ @remarks A number "1" is encoded in the prefix in case we need to upgrade the scheme in future.
+ */
+static NSString *const kAccountPrefix = @"firebase_auth_1_";
+
+@implementation FIRAuthKeychain {
+ /** @var _service
+ @brief The name of the keychain service.
+ */
+ NSString *_service;
+
+ /** @var _legacyItemDeletedForKey
+ @brief Indicates whether or not this class knows that the legacy item for a particular key has
+ been deleted.
+ @remarks This dictionary is to avoid unecessary keychain operations against legacy items.
+ */
+ NSMutableDictionary *_legacyEntryDeletedForKey;
+}
+
+- (id<FIRAuthStorage>)initWithService:(NSString *)service {
+
+#if FIRAUTH_USER_DEFAULTS_STORAGE_AVAILABLE
+
+ NSString *OSVersion = [UIDevice currentDevice].systemVersion;
+ NSRegularExpression *regex =
+ [NSRegularExpression regularExpressionWithPattern:kOSVersionMatcherForUsingUserDefaults
+ options:0
+ error:NULL];
+ if ([regex numberOfMatchesInString:OSVersion options:0 range:NSMakeRange(0, OSVersion.length)]) {
+ return (id<FIRAuthStorage>)[[FIRAuthUserDefaultsStorage alloc] initWithService:service];
+ }
+
+#endif // FIRAUTH_USER_DEFAULTS_STORAGE_AVAILABLE
+
+ self = [super init];
+ if (self) {
+ _service = [service copy];
+ _legacyEntryDeletedForKey = [[NSMutableDictionary alloc] init];
+ }
+ return self;
+}
+
+- (NSData *)dataForKey:(NSString *)key error:(NSError **_Nullable)error {
+ if (!key.length) {
+ [NSException raise:NSInvalidArgumentException
+ format:@"%@", @"The key cannot be nil or empty."];
+ return nil;
+ }
+ NSData *data = [self itemWithQuery:[self genericPasswordQueryWithKey:key] error:error];
+ if (error && *error) {
+ return nil;
+ }
+ if (data) {
+ return data;
+ }
+ // Check for legacy form.
+ if (_legacyEntryDeletedForKey[key]) {
+ return nil;
+ }
+ data = [self itemWithQuery:[self legacyGenericPasswordQueryWithKey:key] error:error];
+ if (error && *error) {
+ return nil;
+ }
+ if (!data) {
+ // Mark legacy data as non-existing so we don't have to query it again.
+ _legacyEntryDeletedForKey[key] = @YES;
+ return nil;
+ }
+ // Move the data to current form.
+ if (![self setData:data forKey:key error:error]) {
+ return nil;
+ }
+ [self deleteLegacyItemWithKey:key];
+ return data;
+}
+
+- (BOOL)setData:(NSData *)data forKey:(NSString *)key error:(NSError **_Nullable)error {
+ if (!key.length) {
+ [NSException raise:NSInvalidArgumentException
+ format:@"%@", @"The key cannot be nil or empty."];
+ return NO;
+ }
+ NSDictionary *attributes = @{
+ (__bridge id)kSecValueData : data,
+ (__bridge id)kSecAttrAccessible : (__bridge id)kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly,
+ };
+ return [self setItemWithQuery:[self genericPasswordQueryWithKey:key]
+ attributes:attributes
+ error:error];
+}
+
+- (BOOL)removeDataForKey:(NSString *)key error:(NSError **_Nullable)error {
+ if (!key.length) {
+ [NSException raise:NSInvalidArgumentException
+ format:@"%@", @"The key cannot be nil or empty."];
+ return NO;
+ }
+ if (![self deleteItemWithQuery:[self genericPasswordQueryWithKey:key] error:error]) {
+ return NO;
+ }
+ // Legacy form item, if exists, also needs to be removed, otherwise it will be exposed when
+ // current form item is removed, leading to incorrect semantics.
+ [self deleteLegacyItemWithKey:key];
+ return YES;
+}
+
+#pragma mark - Private
+
+- (NSData *)itemWithQuery:(NSDictionary *)query error:(NSError **_Nullable)error {
+ NSMutableDictionary *returningQuery = [query mutableCopy];
+ returningQuery[(__bridge id)kSecReturnData] = @YES;
+ returningQuery[(__bridge id)kSecReturnAttributes] = @YES;
+ // Using a match limit of 2 means that we can check whether there is more than one item.
+ // If we used a match limit of 1 we would never find out.
+ returningQuery[(__bridge id)kSecMatchLimit] = @2;
+
+ CFArrayRef result = NULL;
+ OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)returningQuery,
+ (CFTypeRef *)&result);
+
+ if (status == noErr && result != NULL) {
+ NSArray *items = (__bridge_transfer NSArray *)result;
+ if (items.count != 1) {
+ if (error) {
+ *error = [FIRAuthErrorUtils keychainErrorWithFunction:@"SecItemCopyMatching"
+ status:status];
+ }
+ return nil;
+ }
+
+ if (error) {
+ *error = nil;
+ }
+ NSDictionary *item = items[0];
+ return item[(__bridge id)kSecValueData];
+ }
+
+ if (status == errSecItemNotFound) {
+ if (error) {
+ *error = nil;
+ }
+ } else {
+ if (error) {
+ *error = [FIRAuthErrorUtils keychainErrorWithFunction:@"SecItemCopyMatching" status:status];
+ }
+ }
+ return nil;
+}
+
+- (BOOL)setItemWithQuery:(NSDictionary *)query
+ attributes:(NSDictionary *)attributes
+ error:(NSError **_Nullable)error {
+ NSMutableDictionary *combined = [attributes mutableCopy];
+ [combined addEntriesFromDictionary:query];
+ BOOL hasItem = NO;
+ OSStatus status = SecItemAdd((__bridge CFDictionaryRef)combined, NULL);
+
+ if (status == errSecDuplicateItem) {
+ hasItem = YES;
+ status = SecItemUpdate((__bridge CFDictionaryRef)query, (__bridge CFDictionaryRef)attributes);
+ }
+
+ if (status == noErr) {
+ return YES;
+ }
+ if (error) {
+ NSString *function = hasItem ? @"SecItemUpdate" : @"SecItemAdd";
+ *error = [FIRAuthErrorUtils keychainErrorWithFunction:function status:status];
+ }
+ return NO;
+}
+
+- (BOOL)deleteItemWithQuery:(NSDictionary *)query error:(NSError **_Nullable)error {
+ OSStatus status = SecItemDelete((__bridge CFDictionaryRef)query);
+ if (status == noErr || status == errSecItemNotFound) {
+ return YES;
+ }
+ if (error) {
+ *error = [FIRAuthErrorUtils keychainErrorWithFunction:@"SecItemDelete" status:status];
+ }
+ return NO;
+}
+
+/** @fn deleteLegacyItemsWithKey:
+ @brief Deletes legacy item from the keychain if it is not already known to be deleted.
+ @param key The key for the item.
+ */
+- (void)deleteLegacyItemWithKey:(NSString *)key {
+ if (_legacyEntryDeletedForKey[key]) {
+ return;
+ }
+ NSDictionary *query = [self legacyGenericPasswordQueryWithKey:key];
+ SecItemDelete((__bridge CFDictionaryRef)query);
+ _legacyEntryDeletedForKey[key] = @YES;
+}
+
+/** @fn genericPasswordQueryWithKey:
+ @brief Returns a keychain query of generic password to be used to manipulate key'ed value.
+ @param key The key for the value being manipulated, used as the account field in the query.
+ */
+- (NSDictionary *)genericPasswordQueryWithKey:(NSString *)key {
+ return @{
+ (__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword,
+ (__bridge id)kSecAttrAccount : [kAccountPrefix stringByAppendingString:key],
+ (__bridge id)kSecAttrService : _service,
+ };
+}
+
+/** @fn legacyGenericPasswordQueryWithKey:
+ @brief Returns a keychain query of generic password without service field, which is used by
+ previous version of this class.
+ @param key The key for the value being manipulated, used as the account field in the query.
+ */
+- (NSDictionary *)legacyGenericPasswordQueryWithKey:(NSString *)key {
+ return @{
+ (__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword,
+ (__bridge id)kSecAttrAccount : key,
+ };
+}
+
+@end
diff --git a/Firebase/Auth/Source/FIRAuthNotificationManager.m b/Firebase/Auth/Source/FIRAuthNotificationManager.m
new file mode 100644
index 0000000..0692562
--- /dev/null
+++ b/Firebase/Auth/Source/FIRAuthNotificationManager.m
@@ -0,0 +1,175 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "Private/FIRAuthNotificationManager.h"
+
+#import "FIRLogger.h"
+#import "Private/FIRAuthAppCredential.h"
+#import "Private/FIRAuthAppCredentialManager.h"
+#import "FIRAuthGlobalWorkQueue.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @var kNotificationKey
+ @brief The key to locate payload data in the remote notification.
+ */
+static NSString *const kNotificationDataKey = @"com.google.firebase.auth";
+
+/** @var kNotificationReceiptKey
+ @brief The key for the receipt in the remote notification payload data.
+ */
+static NSString *const kNotificationReceiptKey = @"receipt";
+
+/** @var kNotificationSecretKey
+ @brief The key for the secret in the remote notification payload data.
+ */
+static NSString *const kNotificationSecretKey = @"secret";
+
+/** @var kNotificationProberKey
+ @brief The key for marking the prober in the remote notification payload data.
+ */
+static NSString *const kNotificationProberKey = @"warning";
+
+/** @var kProbingTimeout
+ @brief Timeout for probing whether the app delegate forwards the remote notification to us.
+ */
+static const NSTimeInterval kProbingTimeout = 1;
+
+@implementation FIRAuthNotificationManager {
+ /** @var _application
+ @brief The application.
+ */
+ UIApplication *_application;
+
+ /** @var _appCredentialManager
+ @brief The object to handle app credentials delivered via notification.
+ */
+ FIRAuthAppCredentialManager *_appCredentialManager;
+
+ /** @var _hasCheckedNotificationForwarding
+ @brief Whether notification forwarding has been checked or not.
+ */
+ BOOL _hasCheckedNotificationForwarding;
+
+ /** @var _isNotificationBeingForwarded
+ @brief Whether or not notification is being forwarded
+ */
+ BOOL _isNotificationBeingForwarded;
+
+ /** @var _pendingCallbacks
+ @brief All pending callbacks while a check is being performed.
+ */
+ NSMutableArray<FIRAuthNotificationForwardingCallback> *_pendingCallbacks;
+}
+
+- (instancetype)initWithApplication:(UIApplication *)application
+ appCredentialManager:(FIRAuthAppCredentialManager *)appCredentialManager {
+ self = [super init];
+ if (self) {
+ _application = application;
+ _appCredentialManager = appCredentialManager;
+ _timeout = kProbingTimeout;
+ }
+ return self;
+}
+
+- (void)checkNotificationForwardingWithCallback:(FIRAuthNotificationForwardingCallback)callback {
+ if (_pendingCallbacks) {
+ [_pendingCallbacks addObject:callback];
+ return;
+ }
+ if (_hasCheckedNotificationForwarding) {
+ callback(_isNotificationBeingForwarded);
+ return;
+ }
+ _hasCheckedNotificationForwarding = YES;
+ _pendingCallbacks =
+ [[NSMutableArray<FIRAuthNotificationForwardingCallback> alloc] initWithObjects:callback, nil];
+ dispatch_async(dispatch_get_main_queue(), ^{
+ NSDictionary *proberNotification = @{
+ kNotificationDataKey : @{
+ kNotificationProberKey : @"This fake notification should be forwarded to Firebase Auth."
+ }
+ };
+ if ([_application.delegate respondsToSelector:
+ @selector(application:didReceiveRemoteNotification:fetchCompletionHandler:)]) {
+ [_application.delegate application:_application
+ didReceiveRemoteNotification:proberNotification
+ fetchCompletionHandler:^(UIBackgroundFetchResult result) {}];
+ } else if ([_application.delegate respondsToSelector:
+ @selector(application:didReceiveRemoteNotification:)]) {
+ [_application.delegate application:_application
+ didReceiveRemoteNotification:proberNotification];
+ } else {
+ FIRLogError(kFIRLoggerAuth, @"I-AUT000015",
+ @"The UIApplicationDelegate must handle remote notifcation for phone number "
+ @"authentication to work.");
+ }
+ });
+ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(_timeout * NSEC_PER_SEC)),
+ FIRAuthGlobalWorkQueue(), ^{
+ [self callBack];
+ });
+}
+
+- (BOOL)canHandleNotification:(NSDictionary *)notification {
+ NSDictionary *data = notification[kNotificationDataKey];
+ if ([data isKindOfClass:[NSString class]]) {
+ // Deserialize in case the data is a JSON string.
+ NSData *JSONData = [((NSString *)data) dataUsingEncoding:NSUTF8StringEncoding];
+ data = [NSJSONSerialization JSONObjectWithData:JSONData options:0 error:NULL];
+ }
+ if (![data isKindOfClass:[NSDictionary class]]) {
+ return NO;
+ }
+ if (data[kNotificationProberKey]) {
+ if (!_pendingCallbacks) {
+ // The prober notification probably comes from another instance, so pass it along.
+ return NO;
+ }
+ _isNotificationBeingForwarded = YES;
+ [self callBack];
+ return YES;
+ }
+ NSString *receipt = data[kNotificationReceiptKey];
+ if (![receipt isKindOfClass:[NSString class]]) {
+ return NO;
+ }
+ NSString *secret = data[kNotificationSecretKey];
+ if (![receipt isKindOfClass:[NSString class]]) {
+ return NO;
+ }
+ return [_appCredentialManager canFinishVerificationWithReceipt:receipt secret:secret];
+}
+
+#pragma mark - Internal methods
+
+/** @fn callBack
+ @brief Calls back all pending callbacks with the result of notification forwarding check.
+ */
+- (void)callBack {
+ if (!_pendingCallbacks) {
+ return;
+ }
+ NSArray<FIRAuthNotificationForwardingCallback> *allCallbacks = _pendingCallbacks;
+ _pendingCallbacks = nil;
+ for (FIRAuthNotificationForwardingCallback callback in allCallbacks) {
+ callback(_isNotificationBeingForwarded);
+ }
+};
+
+@end
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/FIRAuthProvider.m b/Firebase/Auth/Source/FIRAuthProvider.m
new file mode 100644
index 0000000..6df86d7
--- /dev/null
+++ b/Firebase/Auth/Source/FIRAuthProvider.m
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+// Declared 'extern' in FIRGoogleAuthProvider.h
+NSString *const FIRGoogleAuthProviderID = @"google.com";
+
+// Declared 'extern' in FIRFacebookAuthProvider.h
+NSString *const FIRFacebookAuthProviderID = @"facebook.com";
+
+// Declared 'extern' in FIREmailAuthProvider.h
+NSString *const FIREmailAuthProviderID = @"password";
+
+// Declared 'extern' in FIREmailAuthProvider.h
+NSString *const FIREmailPasswordAuthProviderID = @"password";
+
+// Declared 'extern' in FIRTwitterAuthProvider.h
+NSString *const FIRTwitterAuthProviderID = @"twitter.com";
+
+// Declared 'extern' in FIRGitHubAuthProvider.h
+NSString *const FIRGitHubAuthProviderID = @"github.com";
+
+// Declared 'extern' in FIRPhoneAuthProvider.h
+NSString *const FIRPhoneAuthProviderID = @"phone";
diff --git a/Firebase/Auth/Source/FIRAuthSerialTaskQueue.m b/Firebase/Auth/Source/FIRAuthSerialTaskQueue.m
new file mode 100644
index 0000000..47e6cd5
--- /dev/null
+++ b/Firebase/Auth/Source/FIRAuthSerialTaskQueue.m
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "Private/FIRAuthSerialTaskQueue.h"
+
+#import "Private/FIRAuthGlobalWorkQueue.h"
+
+@implementation FIRAuthSerialTaskQueue {
+ /** @var _dispatchQueue
+ @brief The asyncronous dispatch queue into which tasks are enqueued and processed
+ serially.
+ */
+ dispatch_queue_t _dispatchQueue;
+}
+
+- (instancetype)init {
+ self = [super init];
+ if (self) {
+ _dispatchQueue = dispatch_queue_create("com.google.firebase.auth.serialTaskQueue", NULL);
+ dispatch_set_target_queue(_dispatchQueue, FIRAuthGlobalWorkQueue());
+ }
+ return self;
+}
+
+- (void)enqueueTask:(FIRAuthSerialTask)task {
+ // This dispatch queue will run tasks serially in FIFO order, as long as it's not suspended.
+ dispatch_async(_dispatchQueue, ^{
+ // But as soon as a task is started, stop other tasks from running until the task calls it's
+ // completion handler, which allows the queue to resume processing of tasks. This allows the
+ // task to perform other asyncronous actions on other dispatch queues and "get back to us" when
+ // all of their sub-tasks are complete.
+ dispatch_suspend(_dispatchQueue);
+ task(^{
+ dispatch_resume(_dispatchQueue);
+ });
+ });
+}
+
+@end
diff --git a/Firebase/Auth/Source/FIRAuthSwiftNameSupport.h b/Firebase/Auth/Source/FIRAuthSwiftNameSupport.h
new file mode 100644
index 0000000..f58bdd7
--- /dev/null
+++ b/Firebase/Auth/Source/FIRAuthSwiftNameSupport.h
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef FIR_SWIFT_NAME
+
+#import <Foundation/Foundation.h>
+
+// NS_SWIFT_NAME can only translate factory methods before the iOS 9.3 SDK.
+// // Wrap it in our own macro if it's a non-compatible SDK.
+#ifdef __IPHONE_9_3
+#define FIR_SWIFT_NAME(X) NS_SWIFT_NAME(X)
+#else
+#define FIR_SWIFT_NAME(X) // Intentionally blank.
+#endif // #ifdef __IPHONE_9_3
+
+#endif // FIR_SWIFT_NAME
diff --git a/Firebase/Auth/Source/FIRAuthUserDefaultsStorage.m b/Firebase/Auth/Source/FIRAuthUserDefaultsStorage.m
new file mode 100644
index 0000000..ad23f41
--- /dev/null
+++ b/Firebase/Auth/Source/FIRAuthUserDefaultsStorage.m
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "Private/FIRAuthUserDefaultsStorage.h"
+
+#if FIRAUTH_USER_DEFAULTS_STORAGE_AVAILABLE
+
+NS_ASSUME_NONNULL_BEGIN
+
+static NSString *const kPersistentDomainNamePrefix = @"com.google.Firebase.Auth.";
+
+@implementation FIRAuthUserDefaultsStorage {
+ /** @var _persistentDomainName
+ @brief The name of the persistent domain in user defaults.
+ */
+ NSString *_persistentDomainName;
+
+ /** @var _storage
+ @brief The backing NSUserDefaults storage for this instance.
+ */
+ NSUserDefaults *_storage;
+}
+
+- (id<FIRAuthStorage>)initWithService:(NSString *)service {
+ self = [super init];
+ if (self) {
+ _persistentDomainName = [kPersistentDomainNamePrefix stringByAppendingString:service];
+ _storage = [[NSUserDefaults alloc] init];
+ }
+ return self;
+}
+
+- (nullable NSData *)dataForKey:(NSString *)key error:(NSError **_Nullable)error {
+ if (error) {
+ *error = nil;
+ }
+ NSDictionary<NSString *, id> *allData = [_storage persistentDomainForName:_persistentDomainName];
+ return allData[key];
+}
+
+- (BOOL)setData:(NSData *)data forKey:(NSString *)key error:(NSError **_Nullable)error {
+ NSMutableDictionary<NSString *, id> *allData =
+ [([_storage persistentDomainForName:_persistentDomainName] ?: @{}) mutableCopy];
+ allData[key] = data;
+ [_storage setPersistentDomain:allData forName:_persistentDomainName];
+ return YES;
+}
+
+- (BOOL)removeDataForKey:(NSString *)key error:(NSError **_Nullable)error {
+ NSMutableDictionary<NSString *, id> *allData =
+ [[_storage persistentDomainForName:_persistentDomainName] mutableCopy];
+ [allData removeObjectForKey:key];
+ [_storage setPersistentDomain:allData forName:_persistentDomainName];
+ return YES;
+}
+
+- (void)clear {
+ [_storage setPersistentDomain:@{} forName:_persistentDomainName];
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
+
+#endif // FIRAUTH_USER_DEFAULTS_STORAGE_AVAILABLE
diff --git a/Firebase/Auth/Source/FIRSecureTokenService.h b/Firebase/Auth/Source/FIRSecureTokenService.h
new file mode 100644
index 0000000..cb29127
--- /dev/null
+++ b/Firebase/Auth/Source/FIRSecureTokenService.h
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @typedef FIRFetchAccessTokenCallback
+ @brief The callback used to return the value of attempting to fetch an access token.
+
+ In the event the operation was successful @c token will be set and @c error will be @c nil.
+ In the event of failure @c token will be @c nil and @c error will be set.
+ @c tokenUpdated indicates whether either the access or the refresh token has been updated.
+
+ The token returned should be considered ephemeral and not cached. It should be used immediately
+ and discarded. All operations that need this token should call fetchAccessToken and do their
+ work from the callback.
+ */
+typedef void(^FIRFetchAccessTokenCallback)(NSString *_Nullable token,
+ NSError *_Nullable error,
+ BOOL tokenUpdated);
+
+/** @class FIRSecureTokenService
+ @brief Provides services for token exchanges and refreshes.
+ */
+@interface FIRSecureTokenService : NSObject <NSSecureCoding>
+
+/** @property rawAccessToken
+ @brief The cached access token.
+ @remarks This method is specifically for providing the access token to internal clients during
+ deserialization and sign-in events, and should not be used to retrieve the access token by
+ anyone else.
+ */
+@property(nonatomic, copy, readonly) NSString *rawAccessToken;
+
+/** @property refreshToken
+ @brief The refresh token for the user, or @c nil if the user has yet completed sign-in flow.
+ */
+@property(nonatomic, copy, readonly, nullable) NSString *refreshToken;
+
+/** @property accessTokenExpirationDate
+ @brief The expiration date of the cached access token.
+ */
+@property(nonatomic, copy, readonly, nullable) NSDate *accessTokenExpirationDate;
+
+/** @fn init
+ @brief Please use @c initWithAPIKey:authorizationCode: .
+ */
+- (instancetype)init NS_UNAVAILABLE;
+
+/** @fn initWithAPIKey:authorizationCode:
+ @brief Creates a @c FIRSecureTokenService with an authroization code.
+ @param APIKey A Google API key for making STS requests.
+ @param authorizationCode An authorization code which needs to be exchanged for STS tokens.
+ */
+- (nullable instancetype)initWithAPIKey:(NSString *)APIKey
+ authorizationCode:(NSString *)authorizationCode;
+
+/** @fn initWithAPIKey:authorizationCode:
+ @brief Creates a @c FIRSecureTokenService with an authroization code.
+ @param APIKey A Google API key for making STS requests.
+ @param accessToken The STS access token.
+ @param accessTokenExpirationDate The approximate expiration date of the access token.
+ @param refreshToken The STS refresh token.
+ */
+- (nullable instancetype)initWithAPIKey:(NSString *)APIKey
+ accessToken:(nullable NSString *)accessToken
+ accessTokenExpirationDate:(nullable NSDate *)accessTokenExpirationDate
+ refreshToken:(NSString *)refreshToken;
+
+/** @fn fetchAccessTokenForcingRefresh:callback:
+ @brief Fetch a fresh ephemeral access token for the ID associated with this instance. The token
+ received in the callback should be considered short lived and not cached.
+ @param forceRefresh Forces the token to be refreshed.
+ @param callback Callback block that will be called to return either the token or an error.
+ Invoked asyncronously on the auth global work queue in the future.
+ */
+- (void)fetchAccessTokenForcingRefresh:(BOOL)forceRefresh
+ callback:(FIRFetchAccessTokenCallback)callback;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/FIRSecureTokenService.m b/Firebase/Auth/Source/FIRSecureTokenService.m
new file mode 100644
index 0000000..e88b41c
--- /dev/null
+++ b/Firebase/Auth/Source/FIRSecureTokenService.m
@@ -0,0 +1,214 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRSecureTokenService.h"
+
+#import "FIRAuth.h"
+#import "Private/FIRAuthKeychain.h"
+#import "Private/FIRAuthSerialTaskQueue.h"
+#import "FIRAuthBackend.h"
+#import "FIRSecureTokenRequest.h"
+#import "FIRSecureTokenResponse.h"
+
+/** @var kAPIKeyCodingKey
+ @brief The key used to encode the APIKey for NSSecureCoding.
+ */
+static NSString *const kAPIKeyCodingKey = @"APIKey";
+
+/** @var kRefreshTokenKey
+ @brief The key used to encode the refresh token for NSSecureCoding.
+ */
+static NSString *const kRefreshTokenKey = @"refreshToken";
+
+/** @var kAccessTokenKey
+ @brief The key used to encode the access token for NSSecureCoding.
+ */
+static NSString *const kAccessTokenKey = @"accessToken";
+
+/** @var kAccessTokenExpirationDateKey
+ @brief The key used to encode the access token expiration date for NSSecureCoding.
+ */
+static NSString *const kAccessTokenExpirationDateKey = @"accessTokenExpirationDate";
+
+/** @var kFiveMinutes
+ @brief Five minutes (in seconds.)
+ */
+static const NSTimeInterval kFiveMinutes = 5 * 60;
+
+@interface FIRSecureTokenService ()
+/** @fn initWithAPIKey:
+ @brief Creates a @c FIRSecureTokenService without a credential.
+ @param APIKey A Google API key for making STS requests.
+ */
+- (nullable instancetype)initWithAPIKey:(NSString *)APIKey NS_DESIGNATED_INITIALIZER;
+@end
+
+@implementation FIRSecureTokenService {
+ /** @var _APIKey
+ @brief A Google API key for making Secure Token Service requests.
+ */
+ NSString *_APIKey;
+
+ /** @var _taskQueue
+ @brief Used to serialize all requests for access tokens.
+ */
+ FIRAuthSerialTaskQueue *_taskQueue;
+
+ /** @var _authorizationCode
+ @brief An authorization code which needs to be exchanged for Secure Token Service tokens.
+ */
+ NSString *_Nullable _authorizationCode;
+
+ /** @var _accessToken
+ @brief The currently cached access token. Or |nil| if no token is currently cached.
+ */
+ NSString *_Nullable _accessToken;
+}
+
+- (instancetype)init {
+ [self doesNotRecognizeSelector:_cmd];
+ return nil;
+}
+
+- (nullable instancetype)initWithAPIKey:(NSString *)APIKey {
+ self = [super init];
+ if (self) {
+ _APIKey = [APIKey copy];
+ _taskQueue = [[FIRAuthSerialTaskQueue alloc] init];
+ }
+ return self;
+}
+
+- (nullable instancetype)initWithAPIKey:(NSString *)APIKey
+ authorizationCode:(NSString *)authorizationCode {
+ self = [self initWithAPIKey:APIKey];
+ if (self) {
+ _authorizationCode = [authorizationCode copy];
+ }
+ return self;
+}
+
+- (nullable instancetype)initWithAPIKey:(NSString *)APIKey
+ accessToken:(nullable NSString *)accessToken
+ accessTokenExpirationDate:(nullable NSDate *)accessTokenExpirationDate
+ refreshToken:(NSString *)refreshToken {
+ self = [self initWithAPIKey:APIKey];
+ if (self) {
+ _accessToken = [accessToken copy];
+ _accessTokenExpirationDate = [accessTokenExpirationDate copy];
+ _refreshToken = [refreshToken copy];
+ }
+ return self;
+}
+
+- (void)fetchAccessTokenForcingRefresh:(BOOL)forceRefresh
+ callback:(FIRFetchAccessTokenCallback)callback {
+ [_taskQueue enqueueTask:^(FIRAuthSerialTaskCompletionBlock complete) {
+ if (!forceRefresh && [self hasValidAccessToken]) {
+ complete();
+ callback(_accessToken, nil, NO);
+ } else {
+ [self requestAccessToken:^(NSString *_Nullable token,
+ NSError *_Nullable error,
+ BOOL tokenUpdated) {
+ complete();
+ callback(token, error, tokenUpdated);
+ }];
+ }
+ }];
+}
+
+- (NSString *)rawAccessToken {
+ return _accessToken;
+}
+
+#pragma mark - NSSecureCoding
+
++ (BOOL)supportsSecureCoding {
+ return YES;
+}
+
+- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder {
+ NSString *APIKey = [aDecoder decodeObjectOfClass:[NSString class] forKey:kAPIKeyCodingKey];
+ NSString *refreshToken = [aDecoder decodeObjectOfClass:[NSString class] forKey:kRefreshTokenKey];
+ NSString *accessToken = [aDecoder decodeObjectOfClass:[NSString class] forKey:kAccessTokenKey];
+ NSDate *accessTokenExpirationDate =
+ [aDecoder decodeObjectOfClass:[NSDate class] forKey:kAccessTokenExpirationDateKey];
+ if (!APIKey || !refreshToken) {
+ return nil;
+ }
+ self = [self initWithAPIKey:APIKey];
+ if (self) {
+ _refreshToken = refreshToken;
+ _accessToken = accessToken;
+ _accessTokenExpirationDate = accessTokenExpirationDate;
+ }
+ return self;
+}
+
+- (void)encodeWithCoder:(NSCoder *)aCoder {
+ [aCoder encodeObject:_APIKey forKey:kAPIKeyCodingKey];
+ // Authorization code is not encoded because it is not long-lived.
+ [aCoder encodeObject:_refreshToken forKey:kRefreshTokenKey];
+ [aCoder encodeObject:_accessToken forKey:kAccessTokenKey];
+ [aCoder encodeObject:_accessTokenExpirationDate forKey:kAccessTokenExpirationDateKey];
+}
+
+#pragma mark - Private methods
+
+/** @fn requestAccessToken:
+ @brief Makes a request to STS for an access token.
+ @details This handles both the case that the token has not been granted yet and that it just
+ needs to be refreshed. The caller is responsible for making sure that this is occurring in
+ a @c _taskQueue task.
+ @param callback Called when the fetch is complete. Invoked asynchronously on the main thread in
+ the future.
+ @remarks Because this method is guaranteed to only be called from tasks enqueued in
+ @c _taskQueue, we do not need any @synchronized guards around access to _accessToken/etc.
+ since only one of those tasks is ever running at a time, and those tasks are the only
+ access to and mutation of these instance variables.
+ */
+- (void)requestAccessToken:(FIRFetchAccessTokenCallback)callback {
+ FIRSecureTokenRequest *request;
+ if (_refreshToken.length) {
+ request = [FIRSecureTokenRequest refreshRequestWithRefreshToken:_refreshToken APIKey:_APIKey];
+ } else {
+ request = [FIRSecureTokenRequest authCodeRequestWithCode:_authorizationCode APIKey:_APIKey];
+ }
+ [FIRAuthBackend secureToken:request
+ callback:^(FIRSecureTokenResponse *_Nullable response,
+ NSError *_Nullable error) {
+ BOOL tokenUpdated = NO;
+ NSString *newAccessToken = response.accessToken;
+ if (newAccessToken.length && ![newAccessToken isEqualToString:_accessToken]) {
+ _accessToken = [newAccessToken copy];
+ _accessTokenExpirationDate = response.approximateExpirationDate;
+ tokenUpdated = YES;
+ }
+ NSString *newRefreshToken = response.refreshToken;
+ if (newRefreshToken.length && ![newRefreshToken isEqualToString:_refreshToken]) {
+ _refreshToken = [newRefreshToken copy];
+ tokenUpdated = YES;
+ }
+ callback(newAccessToken, error, tokenUpdated);
+ }];
+}
+
+- (BOOL)hasValidAccessToken {
+ return _accessToken && [_accessTokenExpirationDate timeIntervalSinceNow] > kFiveMinutes;
+}
+
+@end
diff --git a/Firebase/Auth/Source/FIRUser.h b/Firebase/Auth/Source/FIRUser.h
new file mode 100644
index 0000000..ebe8b81
--- /dev/null
+++ b/Firebase/Auth/Source/FIRUser.h
@@ -0,0 +1,463 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+#import "FIRAuth.h"
+#import "FIRAuthDataResult.h"
+#import "FIRAuthSwiftNameSupport.h"
+#import "FIRUserInfo.h"
+
+@class FIRPhoneAuthCredential;
+@class FIRUserProfileChangeRequest;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @typedef FIRAuthTokenCallback
+ @brief The type of block called when a token is ready for use.
+ @see FIRUser.getIDTokenWithCompletion:
+ @see FIRUser.getIDTokenForcingRefresh:withCompletion:
+
+ @param token Optionally; an access token if the request was successful.
+ @param error Optionally; the error which occurred - or nil if the request was successful.
+
+ @remarks One of: @c token or @c error will always be non-nil.
+ */
+typedef void (^FIRAuthTokenCallback)(NSString *_Nullable token, NSError *_Nullable error)
+ FIR_SWIFT_NAME(AuthTokenCallback);
+
+/** @typedef FIRUserProfileChangeCallback
+ @brief The type of block called when a user profile change has finished.
+
+ @param error Optionally; the error which occurred - or nil if the request was successful.
+ */
+typedef void (^FIRUserProfileChangeCallback)(NSError *_Nullable error)
+ FIR_SWIFT_NAME(UserProfileChangeCallback);
+
+/** @typedef FIRSendEmailVerificationCallback
+ @brief The type of block called when a request to send an email verification has finished.
+
+ @param error Optionally; the error which occurred - or nil if the request was successful.
+ */
+typedef void (^FIRSendEmailVerificationCallback)(NSError *_Nullable error)
+ FIR_SWIFT_NAME(SendEmailVerificationCallback);
+
+/** @class FIRUser
+ @brief Represents a user.
+ @remarks This class is thread-safe.
+ */
+FIR_SWIFT_NAME(User)
+@interface FIRUser : NSObject <FIRUserInfo>
+
+/** @property anonymous
+ @brief Indicates the user represents an anonymous user.
+ */
+@property(nonatomic, readonly, getter=isAnonymous) BOOL anonymous;
+
+/** @property emailVerified
+ @brief Indicates the email address associated with this user has been verified.
+ */
+@property(nonatomic, readonly, getter=isEmailVerified) BOOL emailVerified;
+
+/** @property refreshToken
+ @brief A refresh token; useful for obtaining new access tokens independently.
+ @remarks This property should only be used for advanced scenarios, and is not typically needed.
+ */
+@property(nonatomic, readonly, nullable) NSString *refreshToken;
+
+/** @property providerData
+ @brief Profile data for each identity provider, if any.
+ @remarks This data is cached on sign-in and updated when linking or unlinking.
+ */
+@property(nonatomic, readonly, nonnull) NSArray<id<FIRUserInfo>> *providerData;
+
+/** @fn init
+ @brief This class should not be instantiated.
+ @remarks To retrieve the current user, use @c FIRAuth.currentUser. To sign a user
+ in or out, use the methods on @c FIRAuth.
+ */
+- (instancetype)init NS_UNAVAILABLE;
+
+/** @fn updateEmail:completion:
+ @brief Updates the email address for the user. On success, the cached user profile data is
+ updated.
+ @remarks May fail if there is already an account with this email address that was created using
+ email and password authentication.
+
+ @param email The email address for the user.
+ @param completion Optionally; the block invoked when the user profile change has finished.
+ Invoked asynchronously on the main thread in the future.
+
+ @remarks Possible error codes:
+ <ul>
+ <li>@c FIRAuthErrorCodeInvalidRecipientEmail - Indicates an invalid recipient email was
+ sent in the request.
+ </li>
+ <li>@c FIRAuthErrorCodeInvalidSender - Indicates an invalid sender email is set in
+ the console for this action.
+ </li>
+ <li>@c FIRAuthErrorCodeInvalidMessagePayload - Indicates an invalid email template for
+ sending update email.
+ </li>
+ <li>@c FIRAuthErrorCodeEmailAlreadyInUse - Indicates the email is already in use by another
+ account.
+ </li>
+ <li>@c FIRAuthErrorCodeInvalidEmail - Indicates the email address is malformed.
+ </li>
+ <li>@c FIRAuthErrorCodeRequiresRecentLogin - Updating a user’s email is a security
+ sensitive operation that requires a recent login from the user. This error indicates
+ the user has not signed in recently enough. To resolve, reauthenticate the user by
+ invoking reauthenticateWithCredential:completion: on FIRUser.
+ </li>
+ </ul>
+
+ @remarks See @c FIRAuthErrors for a list of error codes that are common to all FIRUser methods.
+ */
+- (void)updateEmail:(NSString *)email completion:(nullable FIRUserProfileChangeCallback)completion
+ FIR_SWIFT_NAME(updateEmail(to:completion:));
+
+/** @fn updatePassword:completion:
+ @brief Updates the password for the user. On success, the cached user profile data is updated.
+
+ @param password The new password for the user.
+ @param completion Optionally; the block invoked when the user profile change has finished.
+ Invoked asynchronously on the main thread in the future.
+
+ @remarks Possible error codes:
+ <ul>
+ <li>@c FIRAuthErrorCodeOperationNotAllowed - Indicates the administrator disabled
+ sign in with the specified identity provider.
+ </li>
+ <li>@c FIRAuthErrorCodeRequiresRecentLogin - Updating a user’s password is a security
+ sensitive operation that requires a recent login from the user. This error indicates
+ the user has not signed in recently enough. To resolve, reauthenticate the user by
+ invoking reauthenticateWithCredential:completion: on FIRUser.
+ </li>
+ <li>@c FIRAuthErrorCodeWeakPassword - Indicates an attempt to set a password that is
+ considered too weak. The NSLocalizedFailureReasonErrorKey field in the NSError.userInfo
+ dictionary object will contain more detailed explanation that can be shown to the user.
+ </li>
+ </ul>
+
+ @remarks See @c FIRAuthErrors for a list of error codes that are common to all FIRUser methods.
+ */
+- (void)updatePassword:(NSString *)password
+ completion:(nullable FIRUserProfileChangeCallback)completion
+ FIR_SWIFT_NAME(updatePassword(to:completion:));
+
+/** @fn updatePhoneNumberCredential:completion:
+ @brief Updates the phone number for the user. On success, the cached user profile data is
+ updated.
+
+ @param phoneNumberCredential The new phone number credential corresponding to the phone number
+ to be added to the firebaes account, if a phone number is already linked to the account this
+ new phone number will replace it.
+ @param completion Optionally; the block invoked when the user profile change has finished.
+ Invoked asynchronously on the main thread in the future.
+
+ @remarks Possible error codes:
+ <ul>
+ <li>@c FIRAuthErrorCodeRequiresRecentLogin - Updating a user’s phone number is a security
+ sensitive operation that requires a recent login from the user. This error indicates
+ the user has not signed in recently enough. To resolve, reauthenticate the user by
+ invoking reauthenticateWithCredential:completion: on FIRUser.
+ </li>
+ </ul>
+
+ @remarks See @c FIRAuthErrors for a list of error codes that are common to all FIRUser methods.
+ */
+- (void)updatePhoneNumberCredential:(FIRPhoneAuthCredential *)phoneNumberCredential
+ completion:(nullable FIRUserProfileChangeCallback)completion;
+
+/** @fn profileChangeRequest
+ @brief Creates an object which may be used to change the user's profile data.
+
+ @remarks Set the properties of the returned object, then call
+ @c FIRUserProfileChangeRequest.commitChangesWithCallback: to perform the updates atomically.
+
+ @return An object which may be used to change the user's profile data atomically.
+ */
+- (FIRUserProfileChangeRequest *)profileChangeRequest FIR_SWIFT_NAME(createProfileChangeRequest());
+
+/** @fn reloadWithCompletion:
+ @brief Reloads the user's profile data from the server.
+
+ @param completion Optionally; the block invoked when the reload has finished. Invoked
+ asynchronously on the main thread in the future.
+
+ @remarks May fail with a @c FIRAuthErrorCodeRequiresRecentLogin error code. In this case
+ you should call @c FIRUser.reauthenticateWithCredential:completion: before re-invoking
+ @c FIRUser.updateEmail:completion:.
+
+ @remarks See @c FIRAuthErrors for a list of error codes that are common to all API methods.
+ */
+- (void)reloadWithCompletion:(nullable FIRUserProfileChangeCallback)completion;
+
+/** @fn reauthenticateWithCredential:completion:
+ @brief Convenience method for @c reauthenticateAndRetrieveDataWithCredential:completion: This
+ method doesn't return additional identity provider data.
+ */
+- (void)reauthenticateWithCredential:(FIRAuthCredential *)credential
+ completion:(nullable FIRUserProfileChangeCallback)completion;
+
+/** @fn reauthenticateWithCredential:completion:
+ @brief Renews the user's authentication tokens by validating a fresh set of credentials supplied
+ by the user and returns additional identity provider data.
+
+ @param credential A user-supplied credential, which will be validated by the server. This can be
+ a successful third-party identity provider sign-in, or an email address and password.
+ @param completion Optionally; the block invoked when the re-authentication operation has
+ finished. Invoked asynchronously on the main thread in the future.
+
+ @remarks If the user associated with the supplied credential is different from the current user,
+ or if the validation of the supplied credentials fails; an error is returned and the current
+ user remains signed in.
+
+ @remarks Possible error codes:
+ <ul>
+ <li>@c FIRAuthErrorCodeInvalidCredential - Indicates the supplied credential is invalid.
+ This could happen if it has expired or it is malformed.
+ </li>
+ <li>@c FIRAuthErrorCodeOperationNotAllowed - Indicates that accounts with the
+ identity provider represented by the credential are not enabled. Enable them in the
+ Auth section of the Firebase console.
+ </li>
+ <li>@c FIRAuthErrorCodeEmailAlreadyInUse - Indicates the email asserted by the credential
+ (e.g. the email in a Facebook access token) is already in use by an existing account,
+ that cannot be authenticated with this method. Call fetchProvidersForEmail for
+ this user’s email and then prompt them to sign in with any of the sign-in providers
+ returned. This error will only be thrown if the "One account per email address"
+ setting is enabled in the Firebase console, under Auth settings. Please note that the
+ error code raised in this specific situation may not be the same on Web and Android.
+ </li>
+ <li>@c FIRAuthErrorCodeUserDisabled - Indicates the user's account is disabled.
+ </li>
+ <li>@c FIRAuthErrorCodeWrongPassword - Indicates the user attempted reauthentication with
+ an incorrect password, if credential is of the type EmailPasswordAuthCredential.
+ </li>
+ <li>@c FIRAuthErrorCodeUserMismatch - Indicates that an attempt was made to
+ reauthenticate with a user which is not the current user.
+ </li>
+ <li>@c FIRAuthErrorCodeInvalidEmail - Indicates the email address is malformed.</li>
+ </ul>
+ @remarks See @c FIRAuthErrors for a list of error codes that are common to all API methods.
+ */
+- (void)reauthenticateAndRetrieveDataWithCredential:(FIRAuthCredential *) credential
+ completion:(nullable FIRAuthDataResultCallback) completion;
+
+/** @fn getIDTokenWithCompletion:
+ @brief Retrieves the Firebase authentication token, possibly refreshing it if it has expired.
+
+ @param completion Optionally; the block invoked when the token is available. Invoked
+ asynchronously on the main thread in the future.
+
+ @remarks See @c FIRAuthErrors for a list of error codes that are common to all API methods.
+ */
+- (void)getIDTokenWithCompletion:(nullable FIRAuthTokenCallback)completion
+ FIR_SWIFT_NAME(getIDToken(completion:));
+
+/** @fn getTokenWithCompletion:
+ @brief Please use @c getIDTokenWithCompletion: instead.
+
+ @param completion Optionally; the block invoked when the token is available. Invoked
+ asynchronously on the main thread in the future.
+
+ @remarks See @c FIRAuthErrors for a list of error codes that are common to all API methods.
+ */
+- (void)getTokenWithCompletion:(nullable FIRAuthTokenCallback)completion
+ FIR_SWIFT_NAME(getToken(completion:)) __attribute__((deprecated));
+
+/** @fn getIDTokenForcingRefresh:completion:
+ @brief Retrieves the Firebase authentication token, possibly refreshing it if it has expired.
+
+ @param forceRefresh Forces a token refresh. Useful if the token becomes invalid for some reason
+ other than an expiration.
+ @param completion Optionally; the block invoked when the token is available. Invoked
+ asynchronously on the main thread in the future.
+
+ @remarks The authentication token will be refreshed (by making a network request) if it has
+ expired, or if @c forceRefresh is YES.
+
+ @remarks See @c FIRAuthErrors for a list of error codes that are common to all API methods.
+ */
+- (void)getIDTokenForcingRefresh:(BOOL)forceRefresh
+ completion:(nullable FIRAuthTokenCallback)completion;
+
+/** @fn getTokenForcingRefresh:completion:
+ @brief Please use getIDTokenForcingRefresh:completion instead.
+
+ @param forceRefresh Forces a token refresh. Useful if the token becomes invalid for some reason
+ other than an expiration.
+ @param completion Optionally; the block invoked when the token is available. Invoked
+ asynchronously on the main thread in the future.
+
+ @remarks The authentication token will be refreshed (by making a network request) if it has
+ expired, or if @c forceRefresh is YES.
+
+ @remarks See @c FIRAuthErrors for a list of error codes that are common to all API methods.
+ */
+- (void)getTokenForcingRefresh:(BOOL)forceRefresh
+ completion:(nullable FIRAuthTokenCallback)completion
+ __attribute__((deprecated));
+
+/** @fn linkWithCredential:completion:
+ @brief Convenience method for @c linkAndRetrieveDataWithCredential:completion: This method
+ doesn't return additional identity provider data.
+ */
+- (void)linkWithCredential:(FIRAuthCredential *)credential
+ completion:(nullable FIRAuthResultCallback)completion;
+
+/** @fn linkAndRetrieveDataWithCredential:completion:
+ @brief Associates a user account from a third-party identity provider with this user and
+ returns additional identity provider data.
+
+ @param credential The credential for the identity provider.
+ @param completion Optionally; the block invoked when the unlinking is complete, or fails.
+ Invoked asynchronously on the main thread in the future.
+
+ @remarks Possible error codes:
+ <ul>
+ <li>@c FIRAuthErrorCodeProviderAlreadyLinked - Indicates an attempt to link a provider of a
+ type already linked to this account.
+ </li>
+ <li>@c FIRAuthErrorCodeCredentialAlreadyInUse - Indicates an attempt to link with a
+ credential
+ that has already been linked with a different Firebase account.
+ </li>
+ <li>@c FIRAuthErrorCodeOperationNotAllowed - Indicates that accounts with the identity
+ provider represented by the credential are not enabled. Enable them in the Auth section
+ of the Firebase console.
+ </li>
+ </ul>
+
+ @remarks This method may also return error codes associated with updateEmail:completion: and
+ updatePassword:completion: on FIRUser.
+
+ @remarks See @c FIRAuthErrors for a list of error codes that are common to all FIRUser methods.
+ */
+- (void)linkAndRetrieveDataWithCredential:(FIRAuthCredential *) credential
+ completion:(nullable FIRAuthDataResultCallback) completion;
+
+/** @fn unlinkFromProvider:completion:
+ @brief Disassociates a user account from a third-party identity provider with this user.
+
+ @param provider The provider ID of the provider to unlink.
+ @param completion Optionally; the block invoked when the unlinking is complete, or fails.
+ Invoked asynchronously on the main thread in the future.
+
+ @remarks Possible error codes:
+ <ul>
+ <li>@c FIRAuthErrorCodeNoSuchProvider - Indicates an attempt to unlink a provider
+ that is not linked to the account.
+ </li>
+ <li>@c FIRAuthErrorCodeRequiresRecentLogin - Updating email is a security sensitive
+ operation that requires a recent login from the user. This error indicates the user
+ has not signed in recently enough. To resolve, reauthenticate the user by invoking
+ reauthenticateWithCredential:completion: on FIRUser.
+ </li>
+ </ul>
+
+ @remarks See @c FIRAuthErrors for a list of error codes that are common to all FIRUser methods.
+ */
+- (void)unlinkFromProvider:(NSString *)provider
+ completion:(nullable FIRAuthResultCallback)completion;
+
+/** @fn sendEmailVerificationWithCompletion:
+ @brief Initiates email verification for the user.
+
+ @param completion Optionally; the block invoked when the request to send an email verification
+ is complete, or fails. Invoked asynchronously on the main thread in the future.
+
+ @remarks Possible error codes:
+ <ul>
+ <li>@c FIRAuthErrorCodeInvalidRecipientEmail - Indicates an invalid recipient email was
+ sent in the request.
+ </li>
+ <li>@c FIRAuthErrorCodeInvalidSender - Indicates an invalid sender email is set in
+ the console for this action.
+ </li>
+ <li>@c FIRAuthErrorCodeInvalidMessagePayload - Indicates an invalid email template for
+ sending update email.
+ </li>
+ <li>@c FIRAuthErrorCodeUserNotFound - Indicates the user account was not found.</li>
+ </ul>
+
+ @remarks See @c FIRAuthErrors for a list of error codes that are common to all FIRUser methods.
+ */
+- (void)sendEmailVerificationWithCompletion:(nullable FIRSendEmailVerificationCallback)completion;
+
+/** @fn deleteWithCompletion:
+ @brief Deletes the user account (also signs out the user, if this was the current user).
+
+ @param completion Optionally; the block invoked when the request to delete the account is
+ complete, or fails. Invoked asynchronously on the main thread in the future.
+
+ @remarks Possible error codes:
+ <ul>
+ <li>@c FIRAuthErrorCodeRequiresRecentLogin - Updating email is a security sensitive
+ operation that requires a recent login from the user. This error indicates the user
+ has not signed in recently enough. To resolve, reauthenticate the user by invoking
+ reauthenticateWithCredential:completion: on FIRUser.
+ </li>
+ </ul>
+
+ @remarks See @c FIRAuthErrors for a list of error codes that are common to all FIRUser methods.
+
+ */
+- (void)deleteWithCompletion:(nullable FIRUserProfileChangeCallback)completion;
+
+@end
+
+/** @class FIRUserProfileChangeRequest
+ @brief Represents an object capable of updating a user's profile data.
+ @remarks Properties are marked as being part of a profile update when they are set. Setting a
+ property value to nil is not the same as leaving the property unassigned.
+ */
+FIR_SWIFT_NAME(UserProfileChangeRequest)
+@interface FIRUserProfileChangeRequest : NSObject
+
+/** @fn init
+ @brief Please use @c FIRUser.profileChangeRequest
+ */
+- (instancetype)init NS_UNAVAILABLE;
+
+/** @property displayName
+ @brief The user's display name.
+ @remarks It is an error to set this property after calling
+ @c FIRUserProfileChangeRequest.commitChangesWithCallback:
+ */
+@property(nonatomic, copy, nullable) NSString *displayName;
+
+/** @property photoURL
+ @brief The user's photo URL.
+ @remarks It is an error to set this property after calling
+ @c FIRUserProfileChangeRequest.commitChangesWithCallback:
+ */
+@property(nonatomic, copy, nullable) NSURL *photoURL;
+
+/** @fn commitChangesWithCompletion:
+ @brief Commits any pending changes.
+ @remarks This method should only be called once. Once called, property values should not be
+ changed.
+
+ @param completion Optionally; the block invoked when the user profile change has been applied.
+ Invoked asynchronously on the main thread in the future.
+ */
+- (void)commitChangesWithCompletion:(nullable FIRUserProfileChangeCallback)completion;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/FIRUser.m b/Firebase/Auth/Source/FIRUser.m
new file mode 100644
index 0000000..f0c3226
--- /dev/null
+++ b/Firebase/Auth/Source/FIRUser.m
@@ -0,0 +1,1170 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "Private/FIRUser_Internal.h"
+
+#import "AuthProviders/EmailPassword/FIREmailPasswordAuthCredential.h"
+#import "AuthProviders/EmailPassword/FIREmailAuthProvider.h"
+#import "AuthProviders/Phone/FIRPhoneAuthCredential_Internal.h"
+#import "AuthProviders/Phone/FIRPhoneAuthProvider.h"
+#import "Private/FIRAdditionalUserInfo_Internal.h"
+#import "FIRAuth.h"
+#import "Private/FIRAuthCredential_Internal.h"
+#import "Private/FIRAuthDataResult_Internal.h"
+#import "Private/FIRAuthErrorUtils.h"
+#import "Private/FIRAuthGlobalWorkQueue.h"
+#import "Private/FIRAuthSerialTaskQueue.h"
+#import "Private/FIRAuth_Internal.h"
+#import "FIRSecureTokenService.h"
+#import "FIRUserInfoImpl.h"
+#import "FIRAuthBackend.h"
+#import "FIRDeleteAccountRequest.h"
+#import "FIRDeleteAccountResponse.h"
+#import "FIRGetAccountInfoRequest.h"
+#import "FIRGetAccountInfoResponse.h"
+#import "FIRGetOOBConfirmationCodeRequest.h"
+#import "FIRGetOOBConfirmationCodeResponse.h"
+#import "FIRSetAccountInfoRequest.h"
+#import "FIRSetAccountInfoResponse.h"
+#import "FIRVerifyAssertionRequest.h"
+#import "FIRVerifyAssertionResponse.h"
+#import "FIRVerifyCustomTokenRequest.h"
+#import "FIRVerifyCustomTokenResponse.h"
+#import "FIRVerifyPasswordRequest.h"
+#import "FIRVerifyPasswordResponse.h"
+#import "FIRVerifyPhoneNumberRequest.h"
+#import "FIRVerifyPhoneNumberResponse.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @var kUserIDCodingKey
+ @brief The key used to encode the user ID for NSSecureCoding.
+ */
+static NSString *const kUserIDCodingKey = @"userID";
+
+/** @var kHasEmailPasswordCredentialCodingKey
+ @brief The key used to encode the hasEmailPasswordCredential property for NSSecureCoding.
+ */
+static NSString *const kHasEmailPasswordCredentialCodingKey = @"hasEmailPassword";
+
+/** @var kAnonymousCodingKey
+ @brief The key used to encode the anonymous property for NSSecureCoding.
+ */
+static NSString *const kAnonymousCodingKey = @"anonymous";
+
+/** @var kEmailCodingKey
+ @brief The key used to encode the email property for NSSecureCoding.
+ */
+static NSString *const kEmailCodingKey = @"email";
+
+/** @var kEmailVerifiedCodingKey
+ @brief The key used to encode the isEmailVerified property for NSSecureCoding.
+ */
+static NSString *const kEmailVerifiedCodingKey = @"emailVerified";
+
+/** @var kDisplayNameCodingKey
+ @brief The key used to encode the displayName property for NSSecureCoding.
+ */
+static NSString *const kDisplayNameCodingKey = @"displayName";
+
+/** @var kPhotoURLCodingKey
+ @brief The key used to encode the photoURL property for NSSecureCoding.
+ */
+static NSString *const kPhotoURLCodingKey = @"photoURL";
+
+/** @var kProviderDataKey
+ @brief The key used to encode the providerData instance variable for NSSecureCoding.
+ */
+static NSString *const kProviderDataKey = @"providerData";
+
+/** @var kAPIKeyCodingKey
+ @brief The key used to encode the APIKey instance variable for NSSecureCoding.
+ */
+static NSString *const kAPIKeyCodingKey = @"APIKey";
+
+/** @var kTokenServiceCodingKey
+ @brief The key used to encode the tokenService instance variable for NSSecureCoding.
+ */
+static NSString *const kTokenServiceCodingKey = @"tokenService";
+
+/** @var kMissingUsersErrorMessage
+ @brief The error message when there is no users array in the getAccountInfo response.
+ */
+static NSString *const kMissingUsersErrorMessage = @"users";
+
+/** @typedef CallbackWithError
+ @brief The type for a callback block that only takes an error parameter.
+ */
+typedef void (^CallbackWithError)(NSError *_Nullable);
+
+/** @typedef CallbackWithUserAndError
+ @brief The type for a callback block that takes a user parameter and an error parameter.
+ */
+typedef void (^CallbackWithUserAndError)(FIRUser *_Nullable, NSError *_Nullable);
+
+/** @typedef CallbackWithUserAndError
+ @brief The type for a callback block that takes a user parameter and an error parameter.
+ */
+typedef void (^CallbackWithAuthDataResultAndError)(FIRAuthDataResult *_Nullable,
+ NSError *_Nullable);
+
+/** @var kMissingPasswordReason
+ @brief The reason why the @c FIRAuthErrorCodeWeakPassword error is thrown.
+ @remarks This error message will be localized in the future.
+ */
+static NSString *const kMissingPasswordReason = @"Missing Password";
+
+/** @fn callInMainThreadWithError
+ @brief Calls a callback in main thread with error.
+ @param callback The callback to be called in main thread.
+ @param error The error to pass to callback.
+ */
+static void callInMainThreadWithError(_Nullable CallbackWithError callback,
+ NSError *_Nullable error) {
+ if (callback) {
+ dispatch_async(dispatch_get_main_queue(), ^{
+ callback(error);
+ });
+ }
+}
+
+/** @fn callInMainThreadWithUserAndError
+ @brief Calls a callback in main thread with user and error.
+ @param callback The callback to be called in main thread.
+ @param user The user to pass to callback if there is no error.
+ @param error The error to pass to callback.
+ */
+static void callInMainThreadWithUserAndError(_Nullable CallbackWithUserAndError callback,
+ FIRUser *_Nonnull user,
+ NSError *_Nullable error) {
+ if (callback) {
+ dispatch_async(dispatch_get_main_queue(), ^{
+ callback(error ? nil : user, error);
+ });
+ }
+}
+
+/** @fn callInMainThreadWithUserAndError
+ @brief Calls a callback in main thread with user and error.
+ @param callback The callback to be called in main thread.
+ @param result The result to pass to callback if there is no error.
+ @param error The error to pass to callback.
+ */
+static void callInMainThreadWithAuthDataResultAndError(
+ _Nullable CallbackWithAuthDataResultAndError callback,
+ FIRAuthDataResult *_Nullable result,
+ NSError *_Nullable error) {
+ if (callback) {
+ dispatch_async(dispatch_get_main_queue(), ^{
+ callback(result, error);
+ });
+ }
+}
+
+@interface FIRUserProfileChangeRequest ()
+
+/** @fn initWithUser:
+ @brief Designated initializer.
+ @param user The user for which we are updating profile information.
+ */
+- (nullable instancetype)initWithUser:(FIRUser *)user NS_DESIGNATED_INITIALIZER;
+
+@end
+
+@interface FIRUser ()
+
+/** @fn initWithAPIKey:
+ @brief Designated initializer
+ @param APIKey The client API key for making RPCs.
+ */
+- (nullable instancetype)initWithAPIKey:(NSString *)APIKey NS_DESIGNATED_INITIALIZER;
+
+@end
+
+@implementation FIRUser {
+ /** @var _hasEmailPasswordCredential
+ @brief Whether or not the user can be authenticated by using Firebase email and password.
+ */
+ BOOL _hasEmailPasswordCredential;
+
+ /** @var _providerData
+ @brief Provider specific user data.
+ */
+ NSDictionary<NSString *, FIRUserInfoImpl *> *_providerData;
+
+ /** @var _APIKey
+ @brief The application's API Key.
+ */
+ NSString *_APIKey;
+
+ /** @var _taskQueue
+ @brief Used to serialize the update profile calls.
+ */
+ FIRAuthSerialTaskQueue *_taskQueue;
+
+ /** @var _tokenService
+ @brief A secure token service associated with this user. For performing token exchanges and
+ refreshing access tokens.
+ */
+ FIRSecureTokenService *_tokenService;
+}
+
+#pragma mark - Properties
+
+// Explicitly @synthesize because these properties are defined in FIRUserInfo protocol.
+@synthesize uid = _userID;
+@synthesize displayName = _displayName;
+@synthesize photoURL = _photoURL;
+@synthesize email = _email;
+@synthesize phoneNumber = _phoneNumber;
+
+#pragma mark -
+
++ (void)retrieveUserWithAPIKey:(NSString *)APIKey
+ accessToken:(NSString *)accessToken
+ accessTokenExpirationDate:(NSDate *)accessTokenExpirationDate
+ refreshToken:(NSString *)refreshToken
+ anonymous:(BOOL)anonymous
+ callback:(FIRRetrieveUserCallback)callback {
+ FIRSecureTokenService *tokenService =
+ [[FIRSecureTokenService alloc] initWithAPIKey:APIKey
+ accessToken:accessToken
+ accessTokenExpirationDate:accessTokenExpirationDate
+ refreshToken:refreshToken];
+ FIRUser *user = [[self alloc] initWithAPIKey:APIKey
+ tokenService:tokenService];
+ [user internalGetTokenWithCallback:^(NSString *_Nullable accessToken, NSError *_Nullable error) {
+ if (error) {
+ callback(nil, error);
+ return;
+ }
+ FIRGetAccountInfoRequest *getAccountInfoRequest =
+ [[FIRGetAccountInfoRequest alloc] initWithAPIKey:APIKey accessToken:accessToken];
+ [FIRAuthBackend getAccountInfo:getAccountInfoRequest
+ callback:^(FIRGetAccountInfoResponse *_Nullable response,
+ NSError *_Nullable error) {
+ if (error) {
+ callback(nil, error);
+ return;
+ }
+ user->_anonymous = anonymous;
+ [user updateWithGetAccountInfoResponse:response];
+ callback(user, nil);
+ }];
+ }];
+}
+
+- (nullable instancetype)initWithAPIKey:(NSString *)APIKey {
+ self = [super init];
+ if (self) {
+ _APIKey = [APIKey copy];
+ _providerData = @{ };
+ _taskQueue = [[FIRAuthSerialTaskQueue alloc] init];
+ }
+ return self;
+}
+
+- (nullable instancetype)initWithAPIKey:(NSString *)APIKey
+ tokenService:(FIRSecureTokenService *)tokenService {
+ self = [self initWithAPIKey:APIKey];
+ if (self) {
+ _tokenService = tokenService;
+ }
+ return self;
+}
+
+#pragma mark - NSSecureCoding
+
++ (BOOL)supportsSecureCoding {
+ return YES;
+}
+
+- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder {
+ NSString *userID = [aDecoder decodeObjectOfClass:[NSString class] forKey:kUserIDCodingKey];
+ BOOL hasAnonymousKey = [aDecoder containsValueForKey:kAnonymousCodingKey];
+ BOOL anonymous = [aDecoder decodeBoolForKey:kAnonymousCodingKey];
+ BOOL hasEmailPasswordCredential =
+ [aDecoder decodeBoolForKey:kHasEmailPasswordCredentialCodingKey];
+ NSString *displayName =
+ [aDecoder decodeObjectOfClass:[NSString class] forKey:kDisplayNameCodingKey];
+ NSURL *photoURL =
+ [aDecoder decodeObjectOfClass:[NSURL class] forKey:kPhotoURLCodingKey];
+ NSString *email =
+ [aDecoder decodeObjectOfClass:[NSString class] forKey:kEmailCodingKey];
+ BOOL emailVerified = [aDecoder decodeBoolForKey:kEmailVerifiedCodingKey];
+ NSSet *providerDataClasses = [NSSet setWithArray:@[
+ [NSDictionary class],
+ [NSString class],
+ [FIRUserInfoImpl class]
+ ]];
+ NSDictionary<NSString *, FIRUserInfoImpl *> *providerData =
+ [aDecoder decodeObjectOfClasses:providerDataClasses forKey:kProviderDataKey];
+ NSString *APIKey =
+ [aDecoder decodeObjectOfClass:[NSString class] forKey:kAPIKeyCodingKey];
+ FIRSecureTokenService *tokenService =
+ [aDecoder decodeObjectOfClass:[FIRSecureTokenService class] forKey:kTokenServiceCodingKey];
+ if (!userID || !APIKey || !tokenService) {
+ return nil;
+ }
+ self = [self initWithAPIKey:APIKey];
+ if (self) {
+ _tokenService = tokenService;
+ _userID = userID;
+ // Previous version of this code didn't save 'anonymous' bit directly but deduced it from
+ // 'hasEmailPasswordCredential' and 'providerData' instead, so here backward compatibility is
+ // provided to read old format data.
+ _anonymous = hasAnonymousKey ? anonymous : (!hasEmailPasswordCredential && !providerData.count);
+ _hasEmailPasswordCredential = hasEmailPasswordCredential;
+ _email = email;
+ _emailVerified = emailVerified;
+ _displayName = displayName;
+ _photoURL = photoURL;
+ _providerData = providerData;
+ }
+ return self;
+}
+
+- (void)encodeWithCoder:(NSCoder *)aCoder {
+ [aCoder encodeObject:_userID forKey:kUserIDCodingKey];
+ [aCoder encodeBool:_anonymous forKey:kAnonymousCodingKey];
+ [aCoder encodeBool:_hasEmailPasswordCredential forKey:kHasEmailPasswordCredentialCodingKey];
+ [aCoder encodeObject:_providerData forKey:kProviderDataKey];
+ [aCoder encodeObject:_email forKey:kEmailCodingKey];
+ [aCoder encodeBool:_emailVerified forKey:kEmailVerifiedCodingKey];
+ [aCoder encodeObject:_photoURL forKey:kPhotoURLCodingKey];
+ [aCoder encodeObject:_displayName forKey:kDisplayNameCodingKey];
+ [aCoder encodeObject:_APIKey forKey:kAPIKeyCodingKey];
+ [aCoder encodeObject:_tokenService forKey:kTokenServiceCodingKey];
+}
+
+#pragma mark -
+
+- (NSString *)providerID {
+ return @"Firebase";
+}
+
+- (NSArray<id<FIRUserInfo>> *)providerData {
+ return _providerData.allValues;
+}
+
+/** @fn getAccountInfoRefreshingCache:
+ @brief Gets the users's account data from the server, updating our local values.
+ @param callback Invoked when the request to getAccountInfo has completed, or when an error has
+ been detected. Invoked asynchronously on the auth global work queue in the future.
+ */
+- (void)getAccountInfoRefreshingCache:(void(^)(FIRGetAccountInfoResponseUser *_Nullable user,
+ NSError *_Nullable error))callback {
+ [self internalGetTokenWithCallback:^(NSString *_Nullable accessToken, NSError *_Nullable error) {
+ if (error) {
+ callback(nil, error);
+ return;
+ }
+ FIRGetAccountInfoRequest *getAccountInfoRequest =
+ [[FIRGetAccountInfoRequest alloc] initWithAPIKey:_APIKey accessToken:accessToken];
+ [FIRAuthBackend getAccountInfo:getAccountInfoRequest
+ callback:^(FIRGetAccountInfoResponse *_Nullable response,
+ NSError *_Nullable error) {
+ if (error) {
+ callback(nil, error);
+ return;
+ }
+ [self updateWithGetAccountInfoResponse:response];
+ if (![self updateKeychain:&error]) {
+ callback(nil, error);
+ return;
+ }
+ callback(response.users.firstObject, nil);
+ }];
+ }];
+}
+
+- (void)updateWithGetAccountInfoResponse:(FIRGetAccountInfoResponse *)response {
+ FIRGetAccountInfoResponseUser *user = response.users.firstObject;
+ _userID = user.localID;
+ _email = user.email;
+ _emailVerified = user.emailVerified;
+ _displayName = user.displayName;
+ _photoURL = user.photoURL;
+ _phoneNumber = user.phoneNumber;
+ _hasEmailPasswordCredential = user.passwordHash.length > 0;
+
+ NSMutableDictionary<NSString *, FIRUserInfoImpl *> *providerData =
+ [NSMutableDictionary dictionary];
+ for (FIRGetAccountInfoResponseProviderUserInfo *providerUserInfo in user.providerUserInfo) {
+ FIRUserInfoImpl *userInfo =
+ [FIRUserInfoImpl userInfoWithGetAccountInfoResponseProviderUserInfo:providerUserInfo];
+ if (userInfo) {
+ providerData[providerUserInfo.providerID] = userInfo;
+ }
+ }
+ _providerData = [providerData copy];
+}
+
+/** @fn executeUserUpdateWithChanges:callback:
+ @brief Performs a setAccountInfo request by mutating the results of a getAccountInfo response,
+ atomically in regards to other calls to this method.
+ @param changeBlock A block responsible for mutating a template @c FIRSetAccountInfoRequest
+ @param callback A block to invoke when the change is complete. Invoked asynchronously on the
+ auth global work queue in the future.
+ */
+- (void)executeUserUpdateWithChanges:(void(^)(FIRGetAccountInfoResponseUser *,
+ FIRSetAccountInfoRequest *))changeBlock
+ callback:(nonnull FIRUserProfileChangeCallback)callback {
+ [_taskQueue enqueueTask:^(FIRAuthSerialTaskCompletionBlock _Nonnull complete) {
+ [self getAccountInfoRefreshingCache:^(FIRGetAccountInfoResponseUser *_Nullable user,
+ NSError *_Nullable error) {
+ if (error) {
+ complete();
+ callback(error);
+ return;
+ }
+ [self internalGetTokenWithCallback:^(NSString *_Nullable accessToken,
+ NSError *_Nullable error) {
+ if (error) {
+ complete();
+ callback(error);
+ return;
+ }
+ // Mutate setAccountInfoRequest in block:
+ FIRSetAccountInfoRequest *setAccountInfoRequest =
+ [[FIRSetAccountInfoRequest alloc] initWithAPIKey:_APIKey];
+ setAccountInfoRequest.accessToken = accessToken;
+ changeBlock(user, setAccountInfoRequest);
+ // Execute request:
+ [FIRAuthBackend setAccountInfo:setAccountInfoRequest
+ callback:^(FIRSetAccountInfoResponse *_Nullable response,
+ NSError *_Nullable error) {
+ if (error) {
+ complete();
+ callback(error);
+ return;
+ }
+ if (response.IDToken && response.refreshToken) {
+ FIRSecureTokenService *tokenService =
+ [[FIRSecureTokenService alloc] initWithAPIKey:_APIKey
+ accessToken:response.IDToken
+ accessTokenExpirationDate:response.approximateExpirationDate
+ refreshToken:response.refreshToken];
+ [self setTokenService:tokenService callback:^(NSError *_Nullable error) {
+ complete();
+ callback(error);
+ }];
+ return;
+ }
+ complete();
+ callback(nil);
+ }];
+ }];
+ }];
+ }];
+}
+
+/** @fn updateKeychain:
+ @brief Updates the keychain for user token or info changes.
+ @param error The error if NO is returned.
+ @return Wether the operation is successful.
+ */
+- (BOOL)updateKeychain:(NSError *_Nullable *_Nullable)error {
+ return !_auth || [_auth updateKeychainWithUser:self error:error];
+}
+
+/** @fn setTokenService:callback:
+ @brief Sets a new token service for the @c FIRUser instance.
+ @param tokenService The new token service object.
+ @param callback The block to be called in the global auth working queue once finished.
+ @remarks The method makes sure the token service has access and refresh token and the new tokens
+ are saved in the keychain before calling back.
+ */
+- (void)setTokenService:(FIRSecureTokenService *)tokenService
+ callback:(nonnull CallbackWithError)callback {
+ [tokenService fetchAccessTokenForcingRefresh:NO
+ callback:^(NSString *_Nullable token,
+ NSError *_Nullable error,
+ BOOL tokenUpdated) {
+ if (error) {
+ callback(error);
+ return;
+ }
+ _tokenService = tokenService;
+ if (![self updateKeychain:&error]) {
+ callback(error);
+ return;
+ }
+ [_auth notifyListenersOfAuthStateChangeWithUser:self token:token];
+ callback(nil);
+ }];
+}
+
+#pragma mark -
+
+/** @fn updateEmail:password:callback:
+ @brief Updates email address and/or password for the current user.
+ @remarks May fail if there is already an email/password-based account for the same email
+ address.
+ @param email The email address for the user, if to be updated.
+ @param password The new password for the user, if to be updated.
+ @param callback The block called when the user profile change has finished. Invoked
+ asynchronously on the auth global work queue in the future.
+ @remarks May fail with a @c FIRAuthErrorCodeRequiresRecentLogin error code.
+ Call @c reauthentateWithCredential:completion: beforehand to avoid this error case.
+ */
+- (void)updateEmail:(nullable NSString *)email
+ password:(nullable NSString *)password
+ callback:(nonnull FIRUserProfileChangeCallback)callback {
+ if (password && ![password length]){
+ callback([FIRAuthErrorUtils weakPasswordErrorWithServerResponseReason:kMissingPasswordReason]);
+ return;
+ }
+ BOOL hadEmailPasswordCredential = _hasEmailPasswordCredential;
+ [self executeUserUpdateWithChanges:^(FIRGetAccountInfoResponseUser *user,
+ FIRSetAccountInfoRequest *request) {
+ if (email) {
+ request.email = email;
+ }
+ if (password) {
+ request.password = password;
+ }
+ }
+ callback:^(NSError *error) {
+ if (error) {
+ callback(error);
+ return;
+ }
+ if (email) {
+ _email = email;
+ }
+ if (_email && password) {
+ _anonymous = NO;
+ _hasEmailPasswordCredential = YES;
+ if (!hadEmailPasswordCredential) {
+ // The list of providers need to be updated for the newly added email-password provider.
+ [self internalGetTokenWithCallback:^(NSString *_Nullable accessToken,
+ NSError *_Nullable error) {
+ if (error) {
+ callback(error);
+ return;
+ }
+ FIRGetAccountInfoRequest *getAccountInfoRequest =
+ [[FIRGetAccountInfoRequest alloc] initWithAPIKey:_APIKey accessToken:accessToken];
+ [FIRAuthBackend getAccountInfo:getAccountInfoRequest
+ callback:^(FIRGetAccountInfoResponse *_Nullable response,
+ NSError *_Nullable error) {
+ if (error) {
+ callback(error);
+ return;
+ }
+ [self updateWithGetAccountInfoResponse:response];
+ if (![self updateKeychain:&error]) {
+ callback(error);
+ return;
+ }
+ callback(nil);
+ }];
+ }];
+ return;
+ }
+ }
+ if (![self updateKeychain:&error]) {
+ callback(error);
+ return;
+ }
+ callback(nil);
+ }];
+}
+
+- (void)updateEmail:(NSString *)email completion:(nullable FIRUserProfileChangeCallback)completion {
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^{
+ [self updateEmail:email password:nil callback:^(NSError *_Nullable error) {
+ callInMainThreadWithError(completion, error);
+ }];
+ });
+}
+
+- (void)updatePassword:(NSString *)password
+ completion:(nullable FIRUserProfileChangeCallback)completion {
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^{
+ [self updateEmail:nil password:password callback:^(NSError *_Nullable error){
+ callInMainThreadWithError(completion, error);
+ }];
+ });
+}
+
+/** @fn internalUpdatePhoneNumberCredential:completion:
+ @brief Updates the phone number for the user. On success, the cached user profile data is
+ updated.
+
+ @param phoneAuthCredential The new phone number credential corresponding to the phone number
+ to be added to the firebaes account, if a phone number is already linked to the account this
+ new phone number will replace it.
+ @param completion Optionally; the block invoked when the user profile change has finished.
+ Invoked asynchronously on the global work queue in the future.
+ */
+- (void)internalUpdatePhoneNumberCredential:(FIRPhoneAuthCredential *)phoneAuthCredential
+ completion:(FIRUserProfileChangeCallback)completion {
+ [self internalGetTokenWithCallback:^(NSString *_Nullable accessToken,
+ NSError *_Nullable error) {
+ if (error) {
+ completion(error);
+ return;
+ }
+ FIRVerifyPhoneNumberRequest *request = [[FIRVerifyPhoneNumberRequest alloc]
+ initWithVerificationID:phoneAuthCredential.verificationID
+ verificationCode:phoneAuthCredential.verificationCode
+ APIKey:_APIKey];
+ request.accessToken = accessToken;
+ [FIRAuthBackend verifyPhoneNumber:request
+ callback:^(FIRVerifyPhoneNumberResponse *_Nullable response,
+ NSError *_Nullable error) {
+ if (error) {
+ completion(error);;
+ return;
+ }
+ // Get account info to update cached user info.
+ [self getAccountInfoRefreshingCache:^(FIRGetAccountInfoResponseUser *_Nullable user,
+ NSError *_Nullable error) {
+ if (![self updateKeychain:&error]) {
+ completion(error);
+ return;
+ }
+ completion(nil);
+ }];
+ }];
+ }];
+}
+
+- (void)updatePhoneNumberCredential:(FIRPhoneAuthCredential *)phoneAuthCredential
+ completion:(nullable FIRUserProfileChangeCallback)completion {
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^{
+ [self internalUpdatePhoneNumberCredential:phoneAuthCredential
+ completion:^(NSError *_Nullable error) {
+ callInMainThreadWithError(completion, error);
+ }];
+ });
+}
+
+- (FIRUserProfileChangeRequest *)profileChangeRequest {
+ __block FIRUserProfileChangeRequest *result;
+ dispatch_sync(FIRAuthGlobalWorkQueue(), ^{
+ result = [[FIRUserProfileChangeRequest alloc] initWithUser:self];
+ });
+ return result;
+}
+
+- (void)setDisplayName:(NSString *)displayName {
+ _displayName = [displayName copy];
+}
+
+- (void)setPhotoURL:(NSURL *)photoURL {
+ _photoURL = [photoURL copy];
+}
+
+- (NSString *)rawAccessToken {
+ return _tokenService.rawAccessToken;
+}
+
+- (NSDate *)accessTokenExpirationDate {
+ return _tokenService.accessTokenExpirationDate;
+}
+
+#pragma mark -
+
+- (void)reloadWithCompletion:(nullable FIRUserProfileChangeCallback)completion {
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^{
+ [self getAccountInfoRefreshingCache:^(FIRGetAccountInfoResponseUser *_Nullable user,
+ NSError *_Nullable error) {
+ callInMainThreadWithError(completion, error);
+ }];
+ });
+}
+
+#pragma mark -
+
+- (void)reauthenticateWithCredential:(FIRAuthCredential *)credential
+ completion:(nullable FIRUserProfileChangeCallback)completion {
+ FIRAuthDataResultCallback callback = ^(FIRAuthDataResult *_Nullable authResult,
+ NSError *_Nullable error) {
+ completion(error);
+ };
+ [self reauthenticateAndRetrieveDataWithCredential:credential completion:callback];
+}
+
+- (void)
+ reauthenticateAndRetrieveDataWithCredential:(FIRAuthCredential *) credential
+ completion:(nullable FIRAuthDataResultCallback) completion {
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^{
+ [_auth internalSignInAndRetrieveDataWithCredential:credential
+ isReauthentication:YES
+ callback:^(FIRAuthDataResult *_Nullable authResult,
+ NSError *_Nullable error) {
+ if (error) {
+ // If "user not found" error returned by backend, translate to user mismatch error which is
+ // more accurate.
+ if (error.code == FIRAuthErrorCodeUserNotFound) {
+ error = [FIRAuthErrorUtils userMismatchError];
+ }
+ callInMainThreadWithAuthDataResultAndError(completion, authResult, error);
+ return;
+ }
+ if (![authResult.user.uid isEqual:_auth.currentUser.uid]) {
+ callInMainThreadWithAuthDataResultAndError(completion, authResult,
+ [FIRAuthErrorUtils userMismatchError]);
+ return;
+ }
+ // Successful reauthenticate
+ [self setTokenService:authResult.user->_tokenService callback:^(NSError *_Nullable error) {
+ callInMainThreadWithAuthDataResultAndError(completion, authResult, error);
+ }];
+ }];
+ });
+}
+
+- (nullable NSString *)refreshToken {
+ __block NSString *result;
+ dispatch_sync(FIRAuthGlobalWorkQueue(), ^{
+ result = _tokenService.refreshToken;
+ });
+ return result;
+}
+
+- (void)getIDTokenWithCompletion:(nullable FIRAuthTokenCallback)completion {
+ // |getTokenForcingRefresh:completion:| is also a public API so there is no need to dispatch to
+ // global work queue here.
+ [self getIDTokenForcingRefresh:NO completion:completion];
+}
+
+- (void)getTokenWithCompletion:(nullable FIRAuthTokenCallback)completion {
+ [self getIDTokenWithCompletion:completion];
+}
+
+- (void)getIDTokenForcingRefresh:(BOOL)forceRefresh
+ completion:(nullable FIRAuthTokenCallback)completion {
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^{
+ [self internalGetTokenForcingRefresh:forceRefresh
+ callback:^(NSString *_Nullable token, NSError *_Nullable error) {
+ if (completion) {
+ dispatch_async(dispatch_get_main_queue(), ^{
+ completion(token, error);
+ });
+ }
+ }];
+ });
+}
+
+- (void)getTokenForcingRefresh:(BOOL)forceRefresh
+ completion:(nullable FIRAuthTokenCallback)completion {
+ [self getIDTokenForcingRefresh:forceRefresh completion:completion];
+}
+
+/** @fn internalGetTokenForcingRefresh:callback:
+ @brief Retrieves the Firebase authentication token, possibly refreshing it if it has expired.
+ @param callback The block to invoke when the token is available. Invoked asynchronously on the
+ global work thread in the future.
+ */
+- (void)internalGetTokenWithCallback:(nonnull FIRAuthTokenCallback)callback {
+ [self internalGetTokenForcingRefresh:NO callback:callback];
+}
+
+- (void)internalGetTokenForcingRefresh:(BOOL)forceRefresh
+ callback:(nonnull FIRAuthTokenCallback)callback {
+ [_tokenService fetchAccessTokenForcingRefresh:forceRefresh
+ callback:^(NSString *_Nullable token,
+ NSError *_Nullable error,
+ BOOL tokenUpdated) {
+ if (error) {
+ callback(nil, error);
+ return;
+ }
+ if (tokenUpdated) {
+ if (![self updateKeychain:&error]) {
+ callback(nil, error);
+ return;
+ }
+ [_auth notifyListenersOfAuthStateChangeWithUser:self token:token];
+ }
+ callback(token, nil);
+ }];
+}
+
+- (void)linkWithCredential:(FIRAuthCredential *)credential
+ completion:(nullable FIRAuthResultCallback)completion {
+ FIRAuthDataResultCallback callback = ^(FIRAuthDataResult *_Nullable authResult,
+ NSError *_Nullable error) {
+ completion(authResult.user, error);
+ };
+ [self linkAndRetrieveDataWithCredential:credential completion:callback];
+}
+
+- (void)linkAndRetrieveDataWithCredential:(FIRAuthCredential *)credential
+ completion:(nullable FIRAuthDataResultCallback)completion {
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^{
+ if (_providerData[credential.provider]) {
+ callInMainThreadWithAuthDataResultAndError(completion,
+ nil,
+ [FIRAuthErrorUtils providerAlreadyLinkedError]);
+ return;
+ }
+ FIRAuthDataResult *result =
+ [[FIRAuthDataResult alloc] initWithUser:self additionalUserInfo:nil];
+ if ([credential isKindOfClass:[FIREmailPasswordAuthCredential class]]) {
+ if (_hasEmailPasswordCredential) {
+ callInMainThreadWithAuthDataResultAndError(completion,
+ nil,
+ [FIRAuthErrorUtils providerAlreadyLinkedError]);
+ return;
+ }
+ FIREmailPasswordAuthCredential *emailPasswordCredential =
+ (FIREmailPasswordAuthCredential *)credential;
+ [self updateEmail:emailPasswordCredential.email
+ password:emailPasswordCredential.password
+ callback:^(NSError *error) {
+ if (error) {
+ callInMainThreadWithAuthDataResultAndError(completion, nil, error);
+ } else {
+ callInMainThreadWithAuthDataResultAndError(completion, result, nil);
+ }
+ }];
+ return;
+ }
+
+ if ([credential isKindOfClass:[FIRPhoneAuthCredential class]]) {
+ FIRPhoneAuthCredential *phoneAuthCredential = (FIRPhoneAuthCredential *)credential;
+ [self internalUpdatePhoneNumberCredential:phoneAuthCredential
+ completion:^(NSError *_Nullable error) {
+ if (error){
+ callInMainThreadWithAuthDataResultAndError(completion, nil, error);
+ } else {
+ callInMainThreadWithAuthDataResultAndError(completion, result, nil);
+ }
+ }];
+ return;
+ }
+
+ [_taskQueue enqueueTask:^(FIRAuthSerialTaskCompletionBlock _Nonnull complete) {
+ CallbackWithAuthDataResultAndError completeWithError =
+ ^(FIRAuthDataResult *result, NSError *error) {
+ complete();
+ callInMainThreadWithAuthDataResultAndError(completion, result, error);
+ };
+ [self internalGetTokenWithCallback:^(NSString *_Nullable accessToken,
+ NSError *_Nullable error) {
+ if (error) {
+ completeWithError(nil, error);
+ return;
+ }
+ FIRVerifyAssertionRequest *request =
+ [[FIRVerifyAssertionRequest alloc] initWithAPIKey:_APIKey providerID:credential.provider];
+ [credential prepareVerifyAssertionRequest:request];
+ request.accessToken = accessToken;
+ [FIRAuthBackend verifyAssertion:request
+ callback:^(FIRVerifyAssertionResponse *response, NSError *error) {
+ if (error) {
+ completeWithError(nil, error);
+ return;
+ }
+ FIRAdditionalUserInfo *additionalUserInfo =
+ [FIRAdditionalUserInfo userInfoWithVerifyAssertionResponse:response];
+ FIRAuthDataResult *result =
+ [[FIRAuthDataResult alloc] initWithUser:self additionalUserInfo:additionalUserInfo];
+ // Update the new token and refresh user info again.
+ _tokenService =
+ [[FIRSecureTokenService alloc] initWithAPIKey:_APIKey
+ accessToken:response.IDToken
+ accessTokenExpirationDate:response.approximateExpirationDate
+ refreshToken:response.refreshToken];
+ [self internalGetTokenWithCallback:^(NSString *_Nullable accessToken,
+ NSError *_Nullable error) {
+ if (error) {
+ completeWithError(nil, error);
+ return;
+ }
+ FIRGetAccountInfoRequest *getAccountInfoRequest =
+ [[FIRGetAccountInfoRequest alloc] initWithAPIKey:_APIKey accessToken:accessToken];
+ [FIRAuthBackend getAccountInfo:getAccountInfoRequest
+ callback:^(FIRGetAccountInfoResponse *_Nullable response,
+ NSError *_Nullable error) {
+ if (error) {
+ completeWithError(nil, error);
+ return;
+ }
+ _anonymous = NO;
+ [self updateWithGetAccountInfoResponse:response];
+ if (![self updateKeychain:&error]) {
+ completeWithError(nil, error);
+ return;
+ }
+ completeWithError(result, nil);
+ }];
+ }];
+ }];
+ }];
+ }];
+ });
+}
+
+- (void)unlinkFromProvider:(NSString *)provider
+ completion:(nullable FIRAuthResultCallback)completion {
+ [_taskQueue enqueueTask:^(FIRAuthSerialTaskCompletionBlock _Nonnull complete) {
+ CallbackWithError completeAndCallbackWithError = ^(NSError *error) {
+ complete();
+ callInMainThreadWithUserAndError(completion, self, error);
+ };
+ [self internalGetTokenWithCallback:^(NSString *_Nullable accessToken,
+ NSError *_Nullable error) {
+ if (error) {
+ completeAndCallbackWithError(error);
+ return;
+ }
+ FIRSetAccountInfoRequest *setAccountInfoRequest =
+ [[FIRSetAccountInfoRequest alloc] initWithAPIKey:_APIKey];
+ setAccountInfoRequest.accessToken = accessToken;
+ BOOL isEmailPasswordProvider = [provider isEqualToString:FIREmailAuthProviderID];
+ if (isEmailPasswordProvider) {
+ if (!_hasEmailPasswordCredential) {
+ completeAndCallbackWithError([FIRAuthErrorUtils noSuchProviderError]);
+ return;
+ }
+ setAccountInfoRequest.deleteAttributes = @[ FIRSetAccountInfoUserAttributePassword ];
+ } else {
+ if (!_providerData[provider]) {
+ completeAndCallbackWithError([FIRAuthErrorUtils noSuchProviderError]);
+ return;
+ }
+ setAccountInfoRequest.deleteProviders = @[ provider ];
+ }
+ [FIRAuthBackend setAccountInfo:setAccountInfoRequest
+ callback:^(FIRSetAccountInfoResponse *_Nullable response,
+ NSError *_Nullable error) {
+ if (error) {
+ completeAndCallbackWithError(error);
+ return;
+ }
+ if (isEmailPasswordProvider) {
+ _hasEmailPasswordCredential = NO;
+ } else {
+ // We can't just use the provider info objects in FIRSetAcccountInfoResponse because they
+ // don't have localID and email fields. Remove the specific provider manually.
+ NSMutableDictionary *mutableProviderData = [_providerData mutableCopy];
+ [mutableProviderData removeObjectForKey:provider];
+ _providerData = [mutableProviderData copy];
+
+ // After successfully unlinking a phone auth provider, remove the phone number from the
+ // cached user info.
+ if ([provider isEqualToString:FIRPhoneAuthProviderID]) {
+ _phoneNumber = nil;
+ }
+ }
+ if (response.IDToken && response.refreshToken) {
+ FIRSecureTokenService *tokenService =
+ [[FIRSecureTokenService alloc] initWithAPIKey:_APIKey
+ accessToken:response.IDToken
+ accessTokenExpirationDate:response.approximateExpirationDate
+ refreshToken:response.refreshToken];
+ [self setTokenService:tokenService callback:^(NSError *_Nullable error) {
+ completeAndCallbackWithError(error);
+ }];
+ return;
+ }
+ if (![self updateKeychain:&error]) {
+ completeAndCallbackWithError(error);
+ return;
+ }
+ completeAndCallbackWithError(nil);
+ }];
+ }];
+ }];
+}
+
+- (void)sendEmailVerificationWithCompletion:(nullable FIRSendEmailVerificationCallback)completion {
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^{
+ [self internalGetTokenWithCallback:^(NSString *_Nullable accessToken,
+ NSError *_Nullable error) {
+ if (error) {
+ callInMainThreadWithError(completion, error);
+ return;
+ }
+ FIRGetOOBConfirmationCodeRequest *request =
+ [FIRGetOOBConfirmationCodeRequest verifyEmailRequestWithAccessToken:accessToken
+ APIKey:_APIKey];
+ [FIRAuthBackend getOOBConfirmationCode:request
+ callback:^(FIRGetOOBConfirmationCodeResponse *_Nullable
+ response,
+ NSError *_Nullable error) {
+ callInMainThreadWithError(completion, error);
+ }];
+ }];
+ });
+}
+
+- (void)deleteWithCompletion:(nullable FIRUserProfileChangeCallback)completion {
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^{
+ [self internalGetTokenWithCallback:^(NSString *_Nullable accessToken,
+ NSError *_Nullable error) {
+ if (error) {
+ callInMainThreadWithError(completion, error);
+ return;
+ }
+ FIRDeleteAccountRequest *deleteUserRequest =
+ [[FIRDeleteAccountRequest alloc] initWithAPIKey:_APIKey
+ localID:_userID
+ accessToken:accessToken];
+ [FIRAuthBackend deleteAccount:deleteUserRequest callback:^(NSError *_Nullable error) {
+ if (error) {
+ callInMainThreadWithError(completion, error);
+ return;
+ }
+ if (![[FIRAuth auth] signOutByForceWithUserID:_userID error:&error]) {
+ callInMainThreadWithError(completion, error);
+ return;
+ }
+ callInMainThreadWithError(completion, error);
+ }];
+ }];
+ });
+}
+
+@end
+
+@implementation FIRUserProfileChangeRequest {
+ /** @var _user
+ @brief The user associated with the change request.
+ */
+ FIRUser *_user;
+
+ /** @var _displayName
+ @brief The display name value to set if @c _displayNameSet is YES.
+ */
+ NSString *_displayName;
+
+ /** @var _displayNameSet
+ @brief Indicates the display name should be part of the change request.
+ */
+ BOOL _displayNameSet;
+
+ /** @var _photoURL
+ @brief The photo URL value to set if @c _displayNameSet is YES.
+ */
+ NSURL *_photoURL;
+
+ /** @var _photoURLSet
+ @brief Indicates the photo URL should be part of the change request.
+ */
+ BOOL _photoURLSet;
+
+ /** @var _consumed
+ @brief Indicates the @c commitChangesWithCallback: method has already been invoked.
+ */
+ BOOL _consumed;
+}
+
+- (nullable instancetype)initWithUser:(FIRUser *)user {
+ self = [super init];
+ if (self) {
+ _user = user;
+ }
+ return self;
+}
+
+- (nullable NSString *)displayName {
+ return _displayName;
+}
+
+- (void)setDisplayName:(nullable NSString *)displayName {
+ dispatch_sync(FIRAuthGlobalWorkQueue(), ^{
+ if (_consumed) {
+ [NSException raise:NSInternalInconsistencyException
+ format:@"%@",
+ @"Invalid call to setDisplayName: after commitChangesWithCallback:."];
+ return;
+ }
+ _displayNameSet = YES;
+ _displayName = [displayName copy];
+ });
+}
+
+- (nullable NSURL *)photoURL {
+ return _photoURL;
+}
+
+- (void)setPhotoURL:(nullable NSURL *)photoURL {
+ dispatch_sync(FIRAuthGlobalWorkQueue(), ^{
+ if (_consumed) {
+ [NSException raise:NSInternalInconsistencyException
+ format:@"%@",
+ @"Invalid call to setPhotoURL: after commitChangesWithCallback:."];
+ return;
+ }
+ _photoURLSet = YES;
+ _photoURL = [photoURL copy];
+ });
+}
+
+/** @fn hasUpdates
+ @brief Indicates at least one field has a value which needs to be committed.
+ */
+- (BOOL)hasUpdates {
+ return _displayNameSet || _photoURLSet;
+}
+
+- (void)commitChangesWithCompletion:(nullable FIRUserProfileChangeCallback)completion {
+ dispatch_sync(FIRAuthGlobalWorkQueue(), ^{
+ if (_consumed) {
+ [NSException raise:NSInternalInconsistencyException
+ format:@"%@",
+ @"commitChangesWithCallback: should only be called once."];
+ return;
+ }
+ _consumed = YES;
+ // Return fast if there is nothing to update:
+ if (![self hasUpdates]) {
+ callInMainThreadWithError(completion, nil);
+ return;
+ }
+ NSString *displayName = [_displayName copy];
+ BOOL displayNameWasSet = _displayNameSet;
+ NSURL *photoURL = [_photoURL copy];
+ BOOL photoURLWasSet = _photoURLSet;
+ [_user executeUserUpdateWithChanges:^(FIRGetAccountInfoResponseUser *user,
+ FIRSetAccountInfoRequest *request) {
+ if (photoURLWasSet) {
+ request.photoURL = photoURL;
+ }
+ if (displayNameWasSet) {
+ request.displayName = displayName;
+ }
+ }
+ callback:^(NSError *_Nullable error) {
+ if (error) {
+ callInMainThreadWithError(completion, error);
+ return;
+ }
+ if (displayNameWasSet) {
+ [_user setDisplayName:displayName];
+ }
+ if (photoURLWasSet) {
+ [_user setPhotoURL:photoURL];
+ }
+ if (![_user updateKeychain:&error]) {
+ callInMainThreadWithError(completion, error);
+ return;
+ }
+ callInMainThreadWithError(completion, nil);
+ }];
+ });
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/FIRUserInfo.h b/Firebase/Auth/Source/FIRUserInfo.h
new file mode 100644
index 0000000..03f2038
--- /dev/null
+++ b/Firebase/Auth/Source/FIRUserInfo.h
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+#import "FIRAuthSwiftNameSupport.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ @brief Represents user data returned from an identity provider.
+ */
+FIR_SWIFT_NAME(UserInfo)
+@protocol FIRUserInfo <NSObject>
+
+/** @property providerID
+ @brief The provider identifier.
+ */
+@property(nonatomic, copy, readonly) NSString *providerID;
+
+/** @property uid
+ @brief The provider's user ID for the user.
+ */
+@property(nonatomic, copy, readonly) NSString *uid;
+
+/** @property displayName
+ @brief The name of the user.
+ */
+@property(nonatomic, copy, readonly, nullable) NSString *displayName;
+
+/** @property photoURL
+ @brief The URL of the user's profile photo.
+ */
+@property(nonatomic, copy, readonly, nullable) NSURL *photoURL;
+
+/** @property email
+ @brief The user's email address.
+ */
+@property(nonatomic, copy, readonly, nullable) NSString *email;
+
+/** @property phoneNumber
+ @brief A phone number associated with the user.
+ @remarks This property is only available for users authenticated via phone number auth.
+ */
+@property(nonatomic, readonly, nullable) NSString *phoneNumber;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/FIRUserInfoImpl.h b/Firebase/Auth/Source/FIRUserInfoImpl.h
new file mode 100644
index 0000000..0022a68
--- /dev/null
+++ b/Firebase/Auth/Source/FIRUserInfoImpl.h
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+#import "FIRUserInfo.h"
+
+@class FIRGetAccountInfoResponseProviderUserInfo;
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FIRUserInfoImpl : NSObject <FIRUserInfo, NSSecureCoding>
+
+/** @fn userInfoWithGetAccountInfoResponseProviderUserInfo:
+ @brief A convenience factory method for constructing a @c FIRUserInfo instance from data
+ returned by the getAccountInfo endpoint.
+ @param providerUserInfo Data returned by the getAccountInfo endpoint.
+ @return A new instance of @c FIRUserInfo using data from the getAccountInfo endpoint.
+ */
++ (nullable instancetype)userInfoWithGetAccountInfoResponseProviderUserInfo:
+ (FIRGetAccountInfoResponseProviderUserInfo *)providerUserInfo;
+
+/** @fn init
+ @brief This class should not be initialized manually.
+ @see FIRUser.providerData
+ */
+- (instancetype)init NS_UNAVAILABLE;
+
+/** @fn initWithProviderID:userID:displayName:photoURL:email:
+ @brief Designated initializer.
+ @param providerID The provider identifier.
+ @param userID The unique user ID for the user (the value of the @c uid field in the token.)
+ @param displayName The name of the user.
+ @param photoURL The URL of the user's profile photo.
+ @param email The user's email address.
+ @param phoneNumber The user's phone number.
+ */
+- (nullable instancetype)initWithProviderID:(NSString *)providerID
+ userID:(NSString *)userID
+ displayName:(nullable NSString *)displayName
+ photoURL:(nullable NSURL *)photoURL
+ email:(nullable NSString *)email
+ phoneNumber:(nullable NSString *)phoneNumber
+ NS_DESIGNATED_INITIALIZER;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/FIRUserInfoImpl.m b/Firebase/Auth/Source/FIRUserInfoImpl.m
new file mode 100644
index 0000000..d172481
--- /dev/null
+++ b/Firebase/Auth/Source/FIRUserInfoImpl.m
@@ -0,0 +1,127 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRUserInfoImpl.h"
+
+#import "FIRGetAccountInfoResponse.h"
+
+/** @var kProviderIDCodingKey
+ @brief The key used to encode the providerID property for NSSecureCoding.
+ */
+static NSString *const kProviderIDCodingKey = @"providerID";
+
+/** @var kUserIDCodingKey
+ @brief The key used to encode the userID property for NSSecureCoding.
+ */
+static NSString *const kUserIDCodingKey = @"userID";
+
+/** @var kDisplayNameCodingKey
+ @brief The key used to encode the displayName property for NSSecureCoding.
+ */
+static NSString *const kDisplayNameCodingKey = @"displayName";
+
+/** @var kProfileURLCodingKey
+ @brief The key used to encode the profileURL property for NSSecureCoding.
+ */
+static NSString *const kProfileURLCodingKey = @"profileURL";
+
+/** @var kPhotoURLCodingKey
+ @brief The key used to encode the photoURL property for NSSecureCoding.
+ */
+static NSString *const kPhotoURLCodingKey = @"photoURL";
+
+/** @var kEmailCodingKey
+ @brief The key used to encode the email property for NSSecureCoding.
+ */
+static NSString *const kEmailCodingKey = @"email";
+
+/** @var kPhoneNumberCodingKey
+ @brief The key used to encode the phoneNumber property for NSSecureCoding.
+ */
+static NSString *const kPhoneNumberCodingKey = @"phoneNumber";
+
+@implementation FIRUserInfoImpl
+
+@synthesize providerID = _providerID;
+@synthesize uid = _userID;
+@synthesize displayName = _displayName;
+@synthesize photoURL = _photoURL;
+@synthesize email = _email;
+@synthesize phoneNumber = _phoneNumber;
+
++ (nullable instancetype)userInfoWithGetAccountInfoResponseProviderUserInfo:
+ (FIRGetAccountInfoResponseProviderUserInfo *)providerUserInfo {
+ return [[self alloc] initWithProviderID:providerUserInfo.providerID
+ userID:providerUserInfo.federatedID
+ displayName:providerUserInfo.displayName
+ photoURL:providerUserInfo.photoURL
+ email:providerUserInfo.email
+ phoneNumber:providerUserInfo.phoneNumber];
+}
+
+- (nullable instancetype)initWithProviderID:(NSString *)providerID
+ userID:(NSString *)userID
+ displayName:(nullable NSString *)displayName
+ photoURL:(nullable NSURL *)photoURL
+ email:(nullable NSString *)email
+ phoneNumber:(nullable NSString *)phoneNumber {
+ self = [super init];
+ if (self) {
+ _providerID = [providerID copy];
+ _userID = [userID copy];
+ _displayName = [displayName copy];
+ _photoURL = [photoURL copy];
+ _email = [email copy];
+ _phoneNumber = [phoneNumber copy];
+ }
+ return self;
+}
+
+#pragma mark - NSSecureCoding
+
++ (BOOL)supportsSecureCoding {
+ return YES;
+}
+
+- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder {
+ NSString *providerID =
+ [aDecoder decodeObjectOfClass:[NSString class] forKey:kProviderIDCodingKey];
+ NSString *userID = [aDecoder decodeObjectOfClass:[NSString class] forKey:kUserIDCodingKey];
+ NSString *displayName =
+ [aDecoder decodeObjectOfClass:[NSString class] forKey:kDisplayNameCodingKey];
+ NSURL *photoURL = [aDecoder decodeObjectOfClass:[NSURL class] forKey:kPhotoURLCodingKey];
+ NSString *email = [aDecoder decodeObjectOfClass:[NSString class] forKey:kEmailCodingKey];
+ NSString *phoneNumber =
+ [aDecoder decodeObjectOfClass:[NSString class] forKey:kPhoneNumberCodingKey];
+
+ return [self initWithProviderID:providerID
+ userID:userID
+ displayName:displayName
+ photoURL:photoURL
+ email:email
+ phoneNumber:phoneNumber];
+}
+
+- (void)encodeWithCoder:(NSCoder *)aCoder {
+ [aCoder encodeObject:_providerID forKey:kProviderIDCodingKey];
+ [aCoder encodeObject:_userID forKey:kUserIDCodingKey];
+ [aCoder encodeObject:_displayName forKey:kDisplayNameCodingKey];
+ [aCoder encodeObject:_photoURL forKey:kPhotoURLCodingKey];
+ [aCoder encodeObject:_email forKey:kEmailCodingKey];
+ [aCoder encodeObject:_phoneNumber forKey:kPhoneNumberCodingKey];
+}
+
+@end
diff --git a/Firebase/Auth/Source/FirebaseAuth.h b/Firebase/Auth/Source/FirebaseAuth.h
new file mode 100644
index 0000000..8dba24e
--- /dev/null
+++ b/Firebase/Auth/Source/FirebaseAuth.h
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIREmailAuthProvider.h"
+#import "FIRFacebookAuthProvider.h"
+#import "FIRGitHubAuthProvider.h"
+#import "FIRGoogleAuthProvider.h"
+#import "FIRTwitterAuthProvider.h"
+#import "FIRAuth.h"
+#import "FIRAuthCredential.h"
+#import "FIRAuthErrors.h"
+#import "FIRUser.h"
+#import "FIRUserInfo.h"
+#import "FirebaseAuthVersion.h"
diff --git a/Firebase/Auth/Source/FirebaseAuthVersion.h b/Firebase/Auth/Source/FirebaseAuthVersion.h
new file mode 100755
index 0000000..1b2d06a
--- /dev/null
+++ b/Firebase/Auth/Source/FirebaseAuthVersion.h
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+/**
+ Version number for FirebaseAuth.
+ */
+extern const double FirebaseAuthVersionNumber;
+
+/**
+ Version string for FirebaseAuth.
+ */
+extern const unsigned char *const FirebaseAuthVersionString;
diff --git a/Firebase/Auth/Source/FirebaseAuthVersion.m b/Firebase/Auth/Source/FirebaseAuthVersion.m
new file mode 100644
index 0000000..fe4055b
--- /dev/null
+++ b/Firebase/Auth/Source/FirebaseAuthVersion.m
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FirebaseAuthVersion.h"
+
+// Convert the macro to a string
+#define STR(x) STR_EXPAND(x)
+#define STR_EXPAND(x) #x
+
+const double FirebaseAuthVersionNumber = FIRAuth_MINOR_VERSION;
+
+const unsigned char *const FirebaseAuthVersionString =
+ (const unsigned char *const)STR(FIRAuth_VERSION);
diff --git a/Firebase/Auth/Source/Private/FIRActionCodeSettings.h b/Firebase/Auth/Source/Private/FIRActionCodeSettings.h
new file mode 100644
index 0000000..adb9cbc
--- /dev/null
+++ b/Firebase/Auth/Source/Private/FIRActionCodeSettings.h
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @class FIRActionCodeSettings
+ @brief Used to set and retrieve settings related to the handling action codes.
+ */
+@interface FIRActionCodeSettings : NSObject
+
+/** @property URL
+ @brief This URL represents the state/Continue URL in the form of a universal link.
+ @remarks This URL can should be contructed as a universal link that would either directly open
+ the app where the action code would be handled or continue to the app after the action code
+ handled by Firebase.
+ */
+@property(nonatomic, copy, nullable) NSURL *URL;
+
+/** @property handleCodeInApp
+ @brief Indicates whether or not the action code link will open the app directly or after being
+ redirected from a Firebase owned web widget.
+ */
+@property(assign, nonatomic) BOOL handleCodeInApp;
+
+/** @property iOSBundleID
+ @brief The iOS bundle ID, if available.
+ */
+@property(copy, nonatomic, readonly, nullable) NSString *iOSBundleID;
+
+/** @property iOSAppStoreID
+ @brief The iOS app store identifier, if available.
+ */
+@property(nonatomic, copy, readonly, nullable) NSString *iOSAppStoreID;
+
+/** @property androidPackageName
+ @brief The Android package name, if available.
+ */
+@property(nonatomic, copy, readonly, nullable) NSString *androidPackageName;
+
+/** @property androidMinimumVersion
+ @brief The minimum Android version supported, if available.
+ */
+@property(nonatomic, copy, readonly, nullable) NSString *androidMinimumVersion;
+
+/** @property androidInstallIfNotAvailable
+ @brief Indicates whether or not the Android app should be installed if not already available.
+ */
+@property(nonatomic, assign, readonly) BOOL androidInstallIfNotAvailable;
+
+/** @fn setIOSBundleID:appStoreID
+ @brief Sets the iOS bundle Id and appStoreID.
+ @param iOSBundleID The iOS bundle ID.
+ @param appStoreID The app's AppStore ID.
+ @remarks If the app is not already installed on an iOS device and an appStoreId is provided, the
+ app store page of the app will be opened. If no app store ID is provided, the web app link
+ will be used instead.
+ */
+- (void)setIOSBundleID:(NSString *)iOSBundleID appStoreID:(nullable NSString *)appStoreID;
+
+/** @fn setAndroidPackageName:installIfNotAvailable:minimumVersion:
+ @brief Sets the Android package name, the flag to indicate whether or not to install the app and
+ the minimum Android version supported.
+ @param androidPackageName The Android package name.
+ @param installIfNotAvailable Indicates whether or not the app should be installed if not
+ available.
+ @param minimumVersion The minimum version of Android supported.
+ @remarks If installIfNotAvailable is set to YES and the link is opened on an android device, it
+ will try to install the app if not already available. Otherwise the web URL is used.
+ */
+- (void)setAndroidPackageName:(NSString *)androidPackageName
+ installIfNotAvailable:(BOOL)installIfNotAvailable
+ minimumVersion:(nullable NSString *)minimumVersion;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/Private/FIRAdditionalUserInfo_Internal.h b/Firebase/Auth/Source/Private/FIRAdditionalUserInfo_Internal.h
new file mode 100644
index 0000000..a813566
--- /dev/null
+++ b/Firebase/Auth/Source/Private/FIRAdditionalUserInfo_Internal.h
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRAdditionalUserInfo.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FIRAdditionalUserInfo () <NSSecureCoding>
+
+/** @fn userInfoWithVerifyAssertionResponse:
+ @brief A convenience factory method for constructing a @c FIRAdditionalUserInfo instance from
+ data returned by the verifyAssertion endpoint.
+ @param verifyAssertionResponse Data returned by the verifyAssertion endpoint.
+ @return A new instance of @c FIRAdditionalUserInfo using data from the verifyAssertion endpoint.
+ */
++ (nullable instancetype)userInfoWithVerifyAssertionResponse:
+ (FIRVerifyAssertionResponse *)verifyAssertionResponse;
+
+/** @fn initWithProviderID:profile:username:
+ @brief Designated initializer.
+ @param providerID The provider identifier.
+ @param profile Dictionary containing the additional IdP specific information.
+ @param username The name of the user.
+ @param isNewUser Indicates whether or not the current user was signed in for the first time.
+ */
+- (nullable instancetype)initWithProviderID:(NSString *)providerID
+ profile:(nullable NSDictionary<NSString *, NSObject *> *)profile
+ username:(nullable NSString *)username
+ isNewUser:(BOOL)isNewUser NS_DESIGNATED_INITIALIZER;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/Private/FIRAuthAPNSToken.h b/Firebase/Auth/Source/Private/FIRAuthAPNSToken.h
new file mode 100644
index 0000000..8efd4e1
--- /dev/null
+++ b/Firebase/Auth/Source/Private/FIRAuthAPNSToken.h
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+#import "FIRAuthAPNSTokenType.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @class FIRAuthAPNSToken
+ @brief A data structure for an APNs token.
+ */
+@interface FIRAuthAPNSToken : NSObject
+
+/** @property data
+ @brief The APNs token data.
+ */
+@property(nonatomic, strong, readonly) NSData *data;
+
+/** @property type
+ @brief The APNs token type.
+ */
+@property(nonatomic, assign, readonly) FIRAuthAPNSTokenType type;
+
+/** @fn initWithData:type:
+ @brief Initializes the instance.
+ @param data The APNs token data.
+ @param type The APNs token type.
+ @return The initialized instance.
+ */
+- (instancetype)initWithData:(NSData *)data type:(FIRAuthAPNSTokenType)type
+ NS_DESIGNATED_INITIALIZER;
+
+/** @fn init
+ @brief Call @c initWithData:type: to get an instance of this class.
+ */
+- (instancetype)init NS_UNAVAILABLE;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/Private/FIRAuthAPNSTokenManager.h b/Firebase/Auth/Source/Private/FIRAuthAPNSTokenManager.h
new file mode 100644
index 0000000..a2d6f4c
--- /dev/null
+++ b/Firebase/Auth/Source/Private/FIRAuthAPNSTokenManager.h
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+#import <UIKit/UIKit.h>
+
+@class FIRAuthAPNSToken;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @typedef FIRAuthAPNSTokenCallback
+ @brief The type of block to receive an APNs token.
+ @param token The APNs token if one is available.
+ */
+typedef void (^FIRAuthAPNSTokenCallback)(FIRAuthAPNSToken *_Nullable token);
+
+/** @class FIRAuthAPNSTokenManager
+ @brief A class to manage APNs token in memory.
+ */
+@interface FIRAuthAPNSTokenManager : NSObject
+
+/** @property token
+ @brief The APNs token, if one is available.
+ @remarks Setting a token with FIRAuthAPNSTokenTypeUnknown will automatically converts it to
+ a token with the automatically detected type.
+ */
+@property(nonatomic, strong, nullable) FIRAuthAPNSToken *token;
+
+/** @property timeout
+ @brief The timeout for registering for remote notification.
+ @remarks Only tests should access this property.
+ */
+@property(nonatomic, assign) NSTimeInterval timeout;
+
+/** @fn init
+ @brief Call @c initWithApplication: to initialize an instance of this class.
+ */
+- (instancetype)init NS_UNAVAILABLE;
+
+/** @fn initWithApplication:bundle
+ @brief Initializes the instance.
+ @param application The @c UIApplication to request the token from.
+ @return The initialized instance.
+ */
+- (instancetype)initWithApplication:(UIApplication *)application NS_DESIGNATED_INITIALIZER;
+
+/** @fn getTokenWithCallback:
+ @brief Attempts to get the APNs token.
+ @param callback The block to be called either immediately or in future, either when a token
+ becomes available, or when timeout occurs, whichever happens earlier.
+ */
+- (void)getTokenWithCallback:(FIRAuthAPNSTokenCallback)callback;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/Private/FIRAuthAppCredential.h b/Firebase/Auth/Source/Private/FIRAuthAppCredential.h
new file mode 100644
index 0000000..57fa83a
--- /dev/null
+++ b/Firebase/Auth/Source/Private/FIRAuthAppCredential.h
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @class FIRAuthAppCredential
+ @brief A class represents a credential that proves the identity of the app.
+ */
+@interface FIRAuthAppCredential : NSObject <NSSecureCoding>
+
+/** @property receipt
+ @brief The server acknowledgement of receiving client's claim of identity.
+ */
+@property(nonatomic, strong, readonly) NSString *receipt;
+
+/** @property secret
+ @brief The secret that the client received from server via a trusted channel, if ever.
+ */
+@property(nonatomic, strong, readonly, nullable) NSString *secret;
+
+/** @fn initWithReceipt:secret:
+ @brief Initializes the instance.
+ @param receipt The server acknowledgement of receiving client's claim of identity.
+ @param secret The secret that the client received from server via a trusted channel, if ever.
+ @return The initialized instance.
+ */
+- (instancetype)initWithReceipt:(NSString *)receipt secret:(nullable NSString *)secret
+ NS_DESIGNATED_INITIALIZER;
+
+/** @fn init
+ @brief Call @c initWithReceipt:secret: to get an instance of this class.
+ */
+- (instancetype)init NS_UNAVAILABLE;
+
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/Private/FIRAuthAppCredentialManager.h b/Firebase/Auth/Source/Private/FIRAuthAppCredentialManager.h
new file mode 100644
index 0000000..21c1545
--- /dev/null
+++ b/Firebase/Auth/Source/Private/FIRAuthAppCredentialManager.h
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+@class FIRAuthAppCredential;
+@class FIRAuthKeychain;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @typedef FIRAuthAppCredentialCallback
+ @brief The type of block to receive an app crdential.
+ @param credential The best available app credential at the time.
+ */
+typedef void (^FIRAuthAppCredentialCallback)(FIRAuthAppCredential *credential);
+
+/** @class FIRAuthAppCredentialManager
+ @brief A class to manage app credentials backed by iOS Keychain.
+ */
+@interface FIRAuthAppCredentialManager : NSObject
+
+/** @property credential
+ @brief The full credential (which has a secret) to be used by the app, if one is available.
+ */
+@property(nonatomic, strong, readonly, nullable) FIRAuthAppCredential *credential;
+
+/** @property maximumNumberOfPendingReceipts
+ @brief The maximum (but not necessarily the minimum) number of pending receipts to be kept.
+ @remarks Only tests should access this property.
+ */
+@property(nonatomic, assign, readonly) NSUInteger maximumNumberOfPendingReceipts;
+
+/** @fn init
+ @brief Call @c initWithKeychain: to initialize an instance of this class.
+ */
+- (instancetype)init NS_UNAVAILABLE;
+
+/** @fn initWithKeychain:
+ @brief Initializes the instance.
+ @param keychain The iOS Keychain storage to back up the app credential with.
+ @return The initialized instance.
+ */
+- (instancetype)initWithKeychain:(FIRAuthKeychain *)keychain NS_DESIGNATED_INITIALIZER;
+
+/** @fn didStartVerificationWithReceipt:timeout:callback:
+ @brief Notifies that the app verification process has started.
+ @param receipt The receipt for verification.
+ @param timeout The timeout value for how long the callback is waited to be called.
+ @param callback The block to be called in future either when the verification finishes, or
+ when timeout occurs, whichever happens earlier.
+ */
+- (void)didStartVerificationWithReceipt:(NSString *)receipt
+ timeout:(NSTimeInterval)timeout
+ callback:(FIRAuthAppCredentialCallback)callback;
+
+/** @fn canFinishVerificationWithReceipt:
+ @brief Attempts to finish verification.
+ @param receipt The receipt to match the original receipt obtained when verification started.
+ @param secret The secret to complete the verification.
+ @return Whether or not the receipt matches a pending verification, and finishes verification
+ if it does.
+ */
+- (BOOL)canFinishVerificationWithReceipt:(NSString *)receipt secret:(NSString *)secret;
+
+/** @fn clearCredential
+ @brief Clears the saved credential, to be used in the case that it is rejected by the server.
+ */
+- (void)clearCredential;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/Private/FIRAuthAppDelegateProxy.h b/Firebase/Auth/Source/Private/FIRAuthAppDelegateProxy.h
new file mode 100644
index 0000000..656e4f2
--- /dev/null
+++ b/Firebase/Auth/Source/Private/FIRAuthAppDelegateProxy.h
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+#import <UIKit/UIKit.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @protocol FIRAuthAppDelegateHandler
+ @brief The protocol to handle app delegate methods.
+ */
+@protocol FIRAuthAppDelegateHandler <NSObject>
+
+/** @fn setAPNSToken:
+ @brief Sets the APNs device token.
+ @param token The APNs device token.
+ */
+- (void)setAPNSToken:(NSData *)token;
+
+/** @fn canHandleNotification:
+ @brief Checks whether the notification can be handled by the receiver, and handles it if so.
+ @param notification The notification in question, which will be consumed if returns @c YES.
+ @return Whether the notification can be (and already has been) handled by the receiver.
+ */
+- (BOOL)canHandleNotification:(nonnull NSDictionary *)notification;
+
+@end
+
+/** @class FIRAuthAppDelegateProxy
+ @brief A manager for swizzling @c UIApplicationDelegate methods.
+ */
+@interface FIRAuthAppDelegateProxy : NSObject
+
+/** @fn initWithApplication
+ @brief Initialize the instance with the given @c UIApplication.
+ @returns An initialized instance, or @c nil if a proxy cannot be established.
+ @remarks This method should only be called from tests if called outside of this class.
+ */
+- (nullable instancetype)initWithApplication:(nullable UIApplication *)application
+ NS_DESIGNATED_INITIALIZER;
+
+/** @fn init
+ @brief Call @c sharedInstance to get an instance of this class.
+ */
+- (instancetype)init NS_UNAVAILABLE;
+
+/** @fn addHandler:
+ @brief Adds a handler for UIApplicationDelegate methods.
+ @param handler The handler to be added.
+ */
+- (void)addHandler:(__weak id<FIRAuthAppDelegateHandler>)handler;
+
+/** @fn sharedInstance
+ @brief Gets the shared instance of this class.
+ @returns The shared instance of this class.
+ */
++ (nullable instancetype)sharedInstance;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/Private/FIRAuthCredential_Internal.h b/Firebase/Auth/Source/Private/FIRAuthCredential_Internal.h
new file mode 100644
index 0000000..e060cda
--- /dev/null
+++ b/Firebase/Auth/Source/Private/FIRAuthCredential_Internal.h
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRAuthCredential.h"
+
+@class FIRVerifyAssertionRequest;
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FIRAuthCredential ()
+
+/** @fn initWithProvider:
+ @brief Designated initializer.
+ @remarks This is the designated initializer for internal/friend subclasses.
+ @param provider The provider name.
+ */
+- (nullable instancetype)initWithProvider:(NSString *)provider NS_DESIGNATED_INITIALIZER;
+
+/** @fn prepareVerifyAssertionRequest:
+ @brief Called immediately before a request to the verifyAssertion endpoint is made. Implementers
+ should update the passed request instance with their credentials.
+ @param request The request to be updated with credentials.
+ */
+- (void)prepareVerifyAssertionRequest:(FIRVerifyAssertionRequest *)request;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/Private/FIRAuthDataResult_Internal.h b/Firebase/Auth/Source/Private/FIRAuthDataResult_Internal.h
new file mode 100644
index 0000000..b95edc2
--- /dev/null
+++ b/Firebase/Auth/Source/Private/FIRAuthDataResult_Internal.h
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRAuthDataResult.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FIRAuthDataResult () <NSSecureCoding>
+
+/** @fn initWithUser:additionalUserInfo:
+ @brief Designated initializer.
+ @param user The signed in user reference.
+ @param additionalUserInfo The additional user info if available.
+ */
+- (nullable instancetype)initWithUser:(FIRUser *)user
+ additionalUserInfo:(nullable FIRAdditionalUserInfo *)additionalUserInfo
+ NS_DESIGNATED_INITIALIZER;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/Private/FIRAuthDispatcher.h b/Firebase/Auth/Source/Private/FIRAuthDispatcher.h
new file mode 100644
index 0000000..f8ddca5
--- /dev/null
+++ b/Firebase/Auth/Source/Private/FIRAuthDispatcher.h
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @typedef FIRAuthDispatcherImplBlock
+ @brief The type of block which can be set as the implementation for @c
+ dispatchAfterDelay:queue:callback: .
+
+ @param delay The delay in seconds after which the task will be scheduled to execute.
+ @param queue The dispatch queue on which the task will be submitted.
+ @param task The task (block) to be scheduled for future execution.
+ */
+typedef void(^FIRAuthDispatcherImplBlock)(NSTimeInterval delay,
+ dispatch_queue_t queue,
+ void (^task)(void));
+
+/** @class FIRAuthDispatchAfter
+ @brief A utility class used to facilitate scheduling tasks to be executed in the future.
+ */
+@interface FIRAuthDispatcher : NSObject
+
+/** @property dispatchAfterImplementation
+ @brief Allows custom implementation of dispatchAfterDelay:queue:callback:.
+ @remarks Set to nil to restore default implementation.
+ */
+@property(nonatomic, nullable, copy) FIRAuthDispatcherImplBlock dispatchAfterImplementation;
+
+/** @fn dispatchAfterDelay:queue:callback:
+ @brief Schedules task in the future after a specified delay.
+
+ @param delay The delay in seconds after which the task will be scheduled to execute.
+ @param queue The dispatch queue on which the task will be submitted.
+ @param task The task (block) to be scheduled for future execution.
+ */
+ - (void)dispatchAfterDelay:(NSTimeInterval)delay
+ queue:(dispatch_queue_t)queue
+ task:(void (^)(void))task;
+
+/** @fn sharedInstance
+ @brief Gets the shared instance of this class.
+ @returns The shared instance of this clss
+ */
++ (instancetype)sharedInstance;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/Private/FIRAuthErrorUtils.h b/Firebase/Auth/Source/Private/FIRAuthErrorUtils.h
new file mode 100644
index 0000000..e2ad4aa
--- /dev/null
+++ b/Firebase/Auth/Source/Private/FIRAuthErrorUtils.h
@@ -0,0 +1,418 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+@class FIRPhoneAuthCredential;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @class FIRAuthErrorUtils
+ @brief Utility class used to construct @c NSError instances.
+ */
+@interface FIRAuthErrorUtils : NSObject
+
+/** @fn RPCRequestEncodingErrorWithUnderlyingError
+ @brief Constructs an @c NSError with the @c FIRAuthInternalErrorCodeRPCRequestEncodingError
+ code and a populated @c NSUnderlyingErrorKey in the @c NSError.userInfo dictionary.
+ @param underlyingError The value of the @c NSUnderlyingErrorKey key.
+ @remarks This error is used when an @c FIRAuthRPCRequest.unencodedHTTPRequestBodyWithError:
+ invocation returns an error. The error returned is wrapped in this internal error code.
+ */
++ (NSError *)RPCRequestEncodingErrorWithUnderlyingError:(NSError *)underlyingError;
+
+/** @fn JSONSerializationErrorForUnencodableType
+ @brief Constructs an @c NSError with the @c FIRAuthErrorCodeJSONSerializationError code.
+ @remarks This error is used when an @c NSJSONSerialization.isValidJSONObject: check fails, not
+ for when an error is returned from @c NSJSONSerialization.dataWithJSONObject:options:error:.
+ */
++ (NSError *)JSONSerializationErrorForUnencodableType;
+
+/** @fn JSONSerializationErrorWithUnderlyingError:
+ @brief Constructs an @c NSError with the @c FIRAuthErrorCodeJSONSerializationError code, and the
+ @c underlyingError as the @c NSUnderlyingErrorKey value in the @c NSError.userInfo
+ dictionary.
+ @param underlyingError The value of the @c NSUnderlyingErrorKey key.
+ @remarks This error is used when an invocation of
+ @c NSJSONSerialization.dataWithJSONObject:options:error: returns an error.
+ */
++ (NSError *)JSONSerializationErrorWithUnderlyingError:(NSError *)underlyingError;
+
+/** @fn networkErrorWithUnderlyingError:
+ @brief Constructs an @c NSError with the @c FIRAuthErrorCodeNetworkError code, and the
+ @c underlyingError as the @c NSUnderlyingErrorKey value in the @c NSError.userInfo
+ dictionary.
+ @param underlyingError The value of the @c NSUnderlyingErrorKey key. Should be the error from
+ GTM.
+ @remarks This error is used when a network request results in an error, and no body data was
+ returned.
+ */
++ (NSError *)networkErrorWithUnderlyingError:(NSError *)underlyingError;
+
+/** @fn unexpectedErrorResponseWithUnderlyingError:
+ @brief Constructs an @c NSError with the @c FIRAuthErrorCodeNetworkError code, and the
+ @c underlyingError as the @c NSUnderlyingErrorKey value.
+ @param data The value of the @c FIRAuthErrorUserInfoDataKey key in the @c NSError.userInfo
+ dictionary.
+ @param underlyingError The value of the @c NSUnderlyingErrorKey key in the @c NSError.userInfo
+ dictionary.
+ @remarks This error is used when a network request results in an error, and unserializable body
+ data was returned.
+ */
++ (NSError *)unexpectedErrorResponseWithData:(NSData *)data
+ underlyingError:(NSError *)underlyingError;
+
+/** @fn unexpectedErrorResponseWithDeserializedResponse:
+ @brief Constructs an @c NSError with the @c FIRAuthInternalErrorCodeUnexpectedErrorResponse
+ code, and a populated @c FIRAuthErrorUserInfoDeserializedResponseKey key in the
+ @c NSError.userInfo dictionary.
+ @param deserializedResponse The value of the @c FIRAuthErrorUserInfoDeserializedResponseKey key.
+ @remarks This error is used when a network request results in an error, and the body data was
+ deserializable as JSON, but couldn't be decoded as an error.
+ */
++ (NSError *)unexpectedErrorResponseWithDeserializedResponse:(id)deserializedResponse;
+
+/** @fn unexpectedResponseWithData:underlyingError:
+ @brief Constructs an @c NSError with the @c FIRAuthInternalErrorCodeUnexpectedResponse
+ code, and a populated @c FIRAuthErrorUserInfoDataKey key in the @c NSError.userInfo
+ dictionary.
+ @param data The value of the @c FIRAuthErrorUserInfoDataKey key in the @c NSError.userInfo
+ dictionary.
+ @param underlyingError The value of the @c NSUnderlyingErrorKey key in the @c NSError.userInfo
+ dictionary.
+ @remarks This error is used when a network request is apparently successful, but the body data
+ couldn't be deserialized as JSON.
+ */
++ (NSError *)unexpectedResponseWithData:(NSData *)data
+ underlyingError:(NSError *)underlyingError;;
+
+/** @fn unexpectedResponseWithDeserializedResponse:
+ @brief Constructs an @c NSError with the @c FIRAuthInternalErrorCodeUnexpectedResponse
+ code, and a populated @c FIRAuthErrorUserInfoDeserializedResponseKey key in the
+ @c NSError.userInfo dictionary.
+ @param deserializedResponse The value of the @c FIRAuthErrorUserInfoDeserializedResponseKey key.
+ @remarks This error is used when a network request is apparently successful, the body data was
+ successfully deserialized as JSON, but the JSON wasn't a dictionary.
+ */
++ (NSError *)unexpectedResponseWithDeserializedResponse:(id)deserializedResponse;
+
+/** @fn unexpectedResponseWithDeserializedResponse:underlyingError:
+ @brief Constructs an @c NSError with the @c FIRAuthInternalErrorCodeUnexpectedResponse
+ code, and populated @c FIRAuthErrorUserInfoDeserializedResponseKey and
+ @c NSUnderlyingErrorKey keys in the @c NSError.userInfo dictionary.
+ @param deserializedResponse The value of the @c FIRAuthErrorUserInfoDeserializedResponseKey key.
+ @param underlyingError The value of the @c NSUnderlyingErrorKey key.
+ @remarks This error is used when a network request was apparently successful, the body data was
+ successfully deserialized as JSON, but the data type of the response was unexpected.
+ */
++ (NSError *)unexpectedResponseWithDeserializedResponse:(nullable id)deserializedResponse
+ underlyingError:(NSError *)underlyingError;
+
+/** @fn RPCResponseDecodingErrorWithDeserializedResponse:underlyingError:
+ @brief Constructs an @c NSError with the @c FIRAuthInternalErrorCodeRPCResponseDecodingError
+ code, and populated @c FIRAuthErrorUserInfoDeserializedResponseKey and
+ @c NSUnderlyingErrorKey keys in the @c NSError.userInfo dictionary.
+ @param deserializedResponse The value of the @c FIRAuthErrorUserInfoDeserializedResponseKey key.
+ @param underlyingError The value of the @c NSUnderlyingErrorKey key.
+ @remarks This error is used when an invocation of @c FIRAuthRPCResponse.setWithDictionary:error:
+ resulted in an error.
+ */
++ (NSError *)RPCResponseDecodingErrorWithDeserializedResponse:(id)deserializedResponse
+ underlyingError:(NSError *)underlyingError;
+
+/** @fn emailAlreadyInUseErrorWithEmail:
+ @brief Constructs an @c NSError with the @c FIRAuthErrorCodeEmailExists code.
+ @param email The email address that is already in use.
+ @return The NSError instance associated with the given FIRAuthError.
+ */
++ (NSError *)emailAlreadyInUseErrorWithEmail:(nullable NSString *)email;
+
+/** @fn userDisabledErrorWithMessageWithMessage:
+ @brief Constructs an @c NSError with the @c FIRAuthErrorCodeUserDisabled code.
+ @param message Error message from the backend, if any.
+ @return The NSError instance associated with the given FIRAuthError.
+ */
++ (NSError *)userDisabledErrorWithMessage:(nullable NSString *)message;
+
+/** @fn wrongPasswordErrorWithMessage:
+ @brief Constructs an @c NSError with the @c FIRAuthErrorCodeWrongPassword code.
+ @param message Error message from the backend, if any.
+ @return The NSError instance associated with the given FIRAuthError.
+ */
++ (NSError *)wrongPasswordErrorWithMessage:(nullable NSString *)message;
+
+/** @fn tooManyRequestsErrorWithMessage:
+ @brief Constructs an @c NSError with the @c FIRAuthErrorCodeTooManyRequests Code.
+ @param message Error message from the backend, if any.
+ @return The NSError instance associated with the given FIRAuthError.
+ */
++ (NSError *)tooManyRequestsErrorWithMessage:(nullable NSString *)message;
+
+/** @fn invalidCustomTokenErrorWithMessage:
+ @brief Constructs an @c NSError with the @c FIRAuthErrorCodeInvalidCustomToken code.
+ @param message Error message from the backend, if any.
+ @return The NSError instance associated with the given FIRAuthError.
+ */
++ (NSError *)invalidCustomTokenErrorWithMessage:(nullable NSString *)message;
+
+/** @fn customTokenMistmatchErrorWithMessage:
+ @brief Constructs an @c NSError with the @c FIRAuthErrorCodeCustomTokenMismatch code.
+ @param message Error message from the backend, if any.
+ @return The NSError instance associated with the given FIRAuthError.
+ */
++ (NSError *)customTokenMistmatchErrorWithMessage:(nullable NSString *)message;
+
+/** @fn invalidCredentialErrorWithMessage:
+ @brief Constructs an @c NSError with the @c FIRAuthErrorCodeInvalidCredential code.
+ @param message Error message from the backend, if any.
+ @return The NSError instance associated with the given FIRAuthError.
+ */
++ (NSError *)invalidCredentialErrorWithMessage:(nullable NSString *)message;
+
+/** @fn requiresRecentLoginError
+ @brief Constructs an @c NSError with the @c FIRAuthErrorCodeRequiresRecentLogin code.
+ @param message Error message from the backend, if any.
+ @return The NSError instance associated with the given FIRAuthError.
+ */
++ (NSError *)requiresRecentLoginErrorWithMessage:(nullable NSString *)message;
+
+/** @fn invalidUserTokenErrorWithMessage:
+ @brief Constructs an @c NSError with the @c FIRAuthErrorCodeInvalidUserToken code.
+ @param message Error message from the backend, if any.
+ @return The NSError instance associated with the given FIRAuthError.
+ */
++ (NSError *)invalidUserTokenErrorWithMessage:(nullable NSString *)message;
+
+/** @fn invalidEmailErrorWithMessage:
+ @brief Constructs an @c NSError with the @c FIRAuthErrorCodeInvalidEmail code.
+ @param message Error message from the backend, if any.
+ @return The NSError instance associated with the given FIRAuthError.
+ */
++ (NSError *)invalidEmailErrorWithMessage:(nullable NSString *)message;
+
+/** @fn accountExistsWithDifferentCredentialErrorWithEmail:
+ @brief Constructs an @c NSError with the @c FIRAuthErrorAccountExistsWithDifferentCredential
+ code.
+ @param Email The email address that is already associated with an existing account
+ @return The NSError instance associated with the given FIRAuthError.
+ */
++ (NSError *)accountExistsWithDifferentCredentialErrorWithEmail:(nullable NSString *)Email;
+
+/** @fn providerAlreadyLinkedErrorWithMessage:
+ @brief Constructs an @c NSError with the @c FIRAuthErrorCodeProviderAlreadyLinked code.
+ @return The NSError instance associated with the given FIRAuthError.
+ */
++ (NSError *)providerAlreadyLinkedError;
+
+/** @fn noSuchProviderError
+ @brief Constructs an @c NSError with the @c FIRAuthErrorCodeNoSuchProvider code.
+ @return The NSError instance associated with the given FIRAuthError.
+ */
++ (NSError *)noSuchProviderError;
+
+/** @fn userTokenExpiredErrorWithMessage:
+ @brief Constructs an @c NSError with the @c FIRAuthErrorCodeUserTokenExpired code.
+ @param message Error message from the backend, if any.
+ @return The NSError instance associated with the given FIRAuthError.
+ */
++ (NSError *)userTokenExpiredErrorWithMessage:(nullable NSString *)message;
+
+/** @fn userNotFoundErrorWithMessage:
+ @brief Constructs an @c NSError with the @c FIRAuthErrorCodeUserNotFound code.
+ @param message Error message from the backend, if any.
+ @return The NSError instance associated with the given FIRAuthError.
+ */
++ (NSError *)userNotFoundErrorWithMessage:(nullable NSString *)message;
+
+/** @fn invalidLocalAPIKeyErrorWithMessage:
+ @brief Constructs an @c NSError with the @c FIRAuthErrorCodeInvalidAPIKey code.
+ @return The NSError instance associated with the given FIRAuthError.
+ */
++ (NSError *)invalidAPIKeyError;
+
+/** @fn userMismatchError
+ @brief Constructs an @c NSError with the @c FIRAuthErrorCodeUserMismatch code.
+ @return The NSError instance associated with the given FIRAuthError.
+ */
++ (NSError *)userMismatchError;
+
+/** @fn credentialAlreadyInUseErrorWithMessage:
+ @brief Constructs an @c NSError with the @c FIRAuthErrorCodeCredentialAlreadyInUse code.
+ @param message Error message from the backend, if any.
+ @param credential Auth credential to be added to the Error User Info dictionary.
+ @return The NSError instance associated with the given FIRAuthError.
+ */
++ (NSError *)credentialAlreadyInUseErrorWithMessage:(nullable NSString *)message
+ credential:(nullable FIRPhoneAuthCredential *)credential;
+
+/** @fn operationNotAllowedErrorWithMessage:
+ @brief Constructs an @c NSError with the @c FIRAuthErrorCodeOperationNotAllowed code.
+ @param message Error message from the backend, if any.
+ @return The NSError instance associated with the given FIRAuthError.
+ */
++ (NSError *)operationNotAllowedErrorWithMessage:(nullable NSString *)message;
+
+/** @fn weakPasswordErrorWithServerResponseReason:
+ @brief Constructs an @c NSError with the @c FIRAuthErrorCodeWeakPassword code.
+ @param serverResponseReason A more detailed explanation string from server response.
+ @return The NSError instance associated with the given FIRAuthError.
+ */
++ (NSError *)weakPasswordErrorWithServerResponseReason:(NSString *)serverResponseReason;
+
+/** @fn appNotAuthorizedError
+ @brief Constructs an @c NSError with the @c FIRAuthErrorCodeAppNotAuthorized code.
+ @return The NSError instance associated with the given FIRAuthError.
+ */
++ (NSError *)appNotAuthorizedError;
+
+/** @fn expiredActionCodeErrorWithMessage:
+ @brief Constructs an @c NSError with the @c FIRAuthErrorCodeExpiredActionCode code.
+ @param message Error message from the backend, if any.
+ @return The NSError instance associated with the given FIRAuthError.
+ */
++ (NSError *)expiredActionCodeErrorWithMessage:(nullable NSString *)message;
+
+/** @fn invalidActionCodeErrorWithMessage:
+ @brief Constructs an @c NSError with the @c FIRAuthErrorCodeInvalidActionCode code.
+ @param message Error message from the backend, if any.
+ @return The NSError instance associated with the given FIRAuthError.
+ */
++ (NSError *)invalidActionCodeErrorWithMessage:(nullable NSString *)message;
+
+/** @fn invalidMessagePayloadError:
+ @brief Constructs an @c NSError with the @c FIRAuthErrorCodeInvalidMessagePayload code.
+ @param message Error message from the backend, if any.
+ @return The NSError instance associated with the given FIRAuthError.
+ */
++ (NSError *)invalidMessagePayloadErrorWithMessage:(nullable NSString *)message;
+
+/** @fn invalidSenderErrorWithMessage:
+ @brief Constructs an @c NSError with the @c FIRAuthErrorCodeInvalidSender code.
+ @param message Error message from the backend, if any.
+ @return The NSError instance associated with the given FIRAuthError.
+ */
++ (NSError *)invalidSenderErrorWithMessage:(nullable NSString *)message;
+
+/** @fn invalidRecipientEmailError:
+ @brief Constructs an @c NSError with the @c FIRAuthErrorCodeInvalidRecipientEmail code.
+ @param message Error message from the backend, if any.
+ @return The NSError instance associated with the given FIRAuthError.
+ */
++ (NSError *)invalidRecipientEmailErrorWithMessage:(nullable NSString *)message;
+
+/** @fn missingPhoneNumberErrorWithMessage:
+ @brief Constructs an @c NSError with the @c FIRAuthErrorCodeMissingPhoneNumber code.
+ @param message Error message from the backend, if any.
+ @return The NSError instance associated with the given FIRAuthError.
+ */
++ (NSError *)missingPhoneNumberErrorWithMessage:(nullable NSString *)message;
+
+/** @fn invalidPhoneNumberErrorWithMessage:
+ @brief Constructs an @c NSError with the @c FIRAuthErrorCodeInvalidPhoneNumber code.
+ @param message Error message from the backend, if any.
+ @return The NSError instance associated with the given FIRAuthError.
+ */
++ (NSError *)invalidPhoneNumberErrorWithMessage:(nullable NSString *)message;
+
+/** @fn missingVerificationCodeErrorWithMessage:
+ @brief Constructs an @c NSError with the @c FIRAuthErrorCodeMissingVerificationCode code.
+ @param message Error message from the backend, if any.
+ @return The NSError instance associated with the given FIRAuthError.
+ */
++ (NSError *)missingVerificationCodeErrorWithMessage:(nullable NSString *)message;
+
+/** @fn invalidVerificationCodeErrorWithMessage:
+ @brief Constructs an @c NSError with the @c FIRAuthErrorCodeInvalidVerificationCode code.
+ @param message Error message from the backend, if any.
+ @return The NSError instance associated with the given FIRAuthError.
+ */
++ (NSError *)invalidVerificationCodeErrorWithMessage:(nullable NSString *)message;
+
+/** @fn missingVerificationIDErrorWithMessage:
+ @brief Constructs an @c NSError with the @c FIRAuthErrorCodeMissingVerificationID code.
+ @param message Error message from the backend, if any.
+ @return The NSError instance associated with the given FIRAuthError.
+ */
++ (NSError *)missingVerificationIDErrorWithMessage:(nullable NSString *)message;
+
+/** @fn invalidVerificationIDErrorWithMessage:
+ @brief Constructs an @c NSError with the @c FIRAuthErrorCodeInvalidVerificationID code.
+ @param message Error message from the backend, if any.
+ @return The NSError instance associated with the given FIRAuthError.
+ */
++ (NSError *)invalidVerificationIDErrorWithMessage:(nullable NSString *)message;
+
+/** @fn sessionExpiredErrorWithMessage:
+ @brief Constructs an @c NSError with the @c FIRAuthErrorCodeSessionExpired code.
+ @param message Error message from the backend, if any.
+ @return The NSError instance associated with the given FIRAuthError.
+ */
++ (NSError *)sessionExpiredErrorWithMessage:(nullable NSString *)message;
+
+/** @fn missingAppCredentialWithMessage:
+ @brief Constructs an @c NSError with the @c FIRAuthErrorMissingCredential code.
+ @param message Error message from the backend, if any.
+ @return The NSError instance associated with the given FIRAuthError.
+ */
++ (NSError *)missingAppCredentialWithMessage:(nullable NSString *)message;
+
+/** @fn invalidAppCredentialWithMessage:
+ @brief Constructs an @c NSError with the @c FIRAuthErrorInvalidCredential code.
+ @param message Error message from the backend, if any.
+ @return The NSError instance associated with the given FIRAuthError.
+ */
++ (NSError *)invalidAppCredentialWithMessage:(nullable NSString *)message;
+
+/** @fn quotaExceededErrorWithMessage:
+ @brief Constructs an @c NSError with the @c FIRAuthErrorCodeQuotaExceeded code.
+ @param message Error message from the backend, if any.
+ @return The NSError instance associated with the given FIRAuthError.
+ */
++ (NSError *)quotaExceededErrorWithMessage:(nullable NSString *)message;
+
+/** @fn missingAppTokenError
+ @brief Constructs an @c NSError with the @c FIRAuthErrorCodeMissingAppToken code.
+ @return The NSError instance associated with the given FIRAuthError.
+ */
++ (NSError *)missingAppTokenError;
+
+/** @fn notificationNotForwardedError
+ @brief Constructs an @c NSError with the @c FIRAuthErrorCodeNotificationNotForwarded code.
+ @return The NSError instance associated with the given FIRAuthError.
+ */
++ (NSError *)notificationNotForwardedError;
+
+/** @fn appNotVerifiedErrorWithMessage:
+ @brief Constructs an @c NSError with the @c FIRAuthErrorCodeAppNotVerified code.
+ @param message Error message from the backend, if any.
+ @return The NSError instance associated with the given FIRAuthError.
+ */
++ (NSError *)appNotVerifiedErrorWithMessage:(nullable NSString *)message;
+
+/** @fn keychainErrorWithFunction:status:
+ @brief Constructs an @c NSError with the @c FIRAuthErrorCodeKeychainError code.
+ @param keychainFunction The keychain function which was invoked and yielded an unexpected
+ response. The @c NSLocalizedFailureReasonErrorKey field in the @c NSError.userInfo
+ dictionary will contain a string partially comprised of this value.
+ @param status The response status from the invoked keychain function. The
+ @c NSLocalizedFailureReasonErrorKey field in the @c NSError.userInfo dictionary will contain
+ a string partially comprised of this value.
+ */
++ (NSError *)keychainErrorWithFunction:(NSString *)keychainFunction status:(OSStatus)status;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/Private/FIRAuthGlobalWorkQueue.h b/Firebase/Auth/Source/Private/FIRAuthGlobalWorkQueue.h
new file mode 100644
index 0000000..0950ff4
--- /dev/null
+++ b/Firebase/Auth/Source/Private/FIRAuthGlobalWorkQueue.h
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @fn FIRAuthGlobalWorkQueue
+ @brief Retrieves the global serial work queue for Firebase Auth.
+ @return The global serial dispatch queue.
+ @remarks To ensure thread safety, all auth code must be executed in either this global work
+ queue, or a serial queue that has its target queue set to this work queue. All public method
+ implementations that may involve contested code shall dispatch to this work queue as the
+ first thing they do.
+ */
+extern dispatch_queue_t FIRAuthGlobalWorkQueue();
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/Private/FIRAuthInternalErrors.h b/Firebase/Auth/Source/Private/FIRAuthInternalErrors.h
new file mode 100644
index 0000000..7eebbde
--- /dev/null
+++ b/Firebase/Auth/Source/Private/FIRAuthInternalErrors.h
@@ -0,0 +1,365 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+#import "FIRAuthErrors.h"
+
+/** @var FIRAuthPublicErrorCodeFlag
+ @brief Bitmask value indicating the error represents a public error code when this bit is
+ zeroed. Error codes which don't contain this flag will be wrapped in an @c NSError whose
+ code is @c FIRAuthErrorCodeInternalError.
+ */
+static const NSInteger FIRAuthPublicErrorCodeFlag = 1 << 20;
+
+/** @var FIRAuthInternalErrorDomain
+ @brief The Firebase Auth error domain for internal errors.
+ */
+extern NSString *const FIRAuthInternalErrorDomain;
+
+/** @var FIRAuthErrorUserInfoDeserializedResponseKey
+ @brief Errors with the code @c FIRAuthErrorCodeUnexpectedResponseError,
+ @c FIRAuthErrorCodeUnexpectedErrorResponseError, and
+ @c FIRAuthInternalErrorCodeRPCResponseDecodingError may contain an @c NSError.userInfo
+ dictionary which contains this key. The value associated with this key is an object of
+ unspecified contents containing the deserialized server response.
+ */
+extern NSString *const FIRAuthErrorUserInfoDeserializedResponseKey;
+
+/** @var FIRAuthErrorUserInfoDataKey
+ @brief Errors with the code @c FIRAuthErrorCodeUnexpectedResponseError or
+ @c FIRAuthErrorCodeUnexpectedErrorResponseError may contain an @c NSError.userInfo
+ dictionary which contains this key. The value associated with this key is an @c NSString
+ which represents the response from a server to an RPC which could not be deserialized.
+ */
+extern NSString *const FIRAuthErrorUserInfoDataKey;
+
+
+/** @var FIRAuthInternalErrorCode
+ @brief Error codes used internally by Firebase Auth.
+ @remarks All errors are generated using an internal error code. These errors are automatically
+ converted to the appropriate public version of the @c NSError by the methods in
+ @c FIRAuthErrorUtils
+ */
+typedef NS_ENUM(NSInteger, FIRAuthInternalErrorCode) {
+ /** @var FIRAuthInternalErrorCodeNetworkError
+ @brief Indicates a network error occurred (such as a timeout, interrupted connection, or
+ unreachable host.)
+ @remarks These types of errors are often recoverable with a retry.
+
+ See the @c NSUnderlyingError value in the @c NSError.userInfo dictionary for details about
+ the network error which occurred.
+ */
+ FIRAuthInternalErrorCodeNetworkError = FIRAuthPublicErrorCodeFlag | FIRAuthErrorCodeNetworkError,
+
+ /** @var FIRAuthInternalErrorCodeEmailAlreadyInUse
+ @brief The email used to attempt a sign-up already exists.
+ */
+ FIRAuthInternalErrorCodeEmailAlreadyInUse =
+ FIRAuthPublicErrorCodeFlag | FIRAuthErrorCodeEmailAlreadyInUse,
+
+ /** @var FIRAuthInternalErrorCodeUserDisabled
+ @brief Indicates the user's account is disabled on the server side.
+ */
+ FIRAuthInternalErrorCodeUserDisabled = FIRAuthPublicErrorCodeFlag | FIRAuthErrorCodeUserDisabled,
+
+ /** @var FIRAuthInternalErrorCodeWrongPassword
+ @brief Indicates the user attempted sign in with a wrong password
+ */
+ FIRAuthInternalErrorCodeWrongPassword =
+ FIRAuthPublicErrorCodeFlag | FIRAuthErrorCodeWrongPassword,
+
+ /** @var FIRAuthInternalErrorCodeKeychainError
+ @brief Indicates an error occurred accessing the keychain.
+ @remarks The @c NSLocalizedFailureReasonErrorKey field in the @c NSError.userInfo dictionary
+ will contain more information about the error encountered.
+ */
+ FIRAuthInternalErrorCodeKeychainError =
+ FIRAuthPublicErrorCodeFlag | FIRAuthErrorCodeKeychainError,
+
+ /** @var FIRAuthInternalErrorCodeInternalError
+ @brief An internal error occurred.
+ @remarks This value is here for consistency. It's also used to make the implementation of
+ wrapping internal errors simpler.
+ */
+ FIRAuthInternalErrorCodeInternalError =
+ FIRAuthPublicErrorCodeFlag | FIRAuthErrorCodeInternalError,
+
+ /** @var FIRAuthInternalErrorCodeTooManyRequests
+ @brief Indicates that too many requests were made to a server method.
+ */
+ FIRAuthInternalErrorCodeTooManyRequests =
+ FIRAuthPublicErrorCodeFlag | FIRAuthErrorCodeTooManyRequests,
+
+ /** @var FIRAuthInternalErrorCodeInvalidCustomToken
+ @brief Indicates a validation error with the custom token.
+ */
+ FIRAuthInternalErrorCodeInvalidCustomToken =
+ FIRAuthPublicErrorCodeFlag | FIRAuthErrorCodeInvalidCustomToken,
+
+ /** @var FIRAuthInternalErrorCodeCredentialMismatch
+ @brief Indicates the service account and the API key belong to different projects.
+ */
+ FIRAuthInternalErrorCodeCustomTokenMismatch =
+ FIRAuthPublicErrorCodeFlag | FIRAuthErrorCodeCustomTokenMismatch,
+
+ /** @var FIRAuthInternalErrorCodeInvalidCredential
+ @brief Indicates the IDP token or requestUri is invalid.
+ */
+ FIRAuthInternalErrorCodeInvalidCredential =
+ FIRAuthPublicErrorCodeFlag | FIRAuthErrorCodeInvalidCredential,
+
+ /** @var FIRAuthInternalErrorCodeRequiresRecentLogin
+ @brief Indicates the user has attemped to change email or password more than 5 minutes after
+ signing in.
+ */
+ FIRAuthInternalErrorCodeRequiresRecentLogin =
+ FIRAuthPublicErrorCodeFlag | FIRAuthErrorCodeRequiresRecentLogin,
+
+ /** @var FIRAuthInternalErrorCodeInvalidUserToken
+ @brief Indicates user's saved auth credential is invalid, the user needs to sign in again.
+ */
+ FIRAuthInternalErrorCodeInvalidUserToken =
+ FIRAuthPublicErrorCodeFlag | FIRAuthErrorCodeInvalidUserToken,
+
+ /** @var FIRAuthInternalErrorCodeInvalidEmail
+ @brief Indicates the email identifier is invalid.
+ */
+ FIRAuthInternalErrorCodeInvalidEmail =
+ FIRAuthPublicErrorCodeFlag | FIRAuthErrorCodeInvalidEmail,
+
+ /** @var FIRAuthInternalErrorCodeAccountExistsWithDifferentCredential
+ @brief Indicates account linking is needed.
+ */
+ FIRAuthInternalErrorCodeAccountExistsWithDifferentCredential =
+ FIRAuthPublicErrorCodeFlag | FIRAuthErrorCodeAccountExistsWithDifferentCredential,
+
+ /** @var FIRAuthInternalErrorCodeProviderAlreadyLinked
+ @brief Indicates an attempt to link a provider to which we are already linked.
+ */
+ FIRAuthInternalErrorCodeProviderAlreadyLinked =
+ FIRAuthPublicErrorCodeFlag | FIRAuthErrorCodeProviderAlreadyLinked,
+
+ /** @var FIRAuthInternalErrorCodeNoSuchProvider
+ @brief Indicates an attempt to unlink a provider that is not is not linked.
+ */
+ FIRAuthInternalErrorCodeNoSuchProvider =
+ FIRAuthPublicErrorCodeFlag | FIRAuthErrorCodeNoSuchProvider,
+
+ /** @var FIRAuthInternalErrorCodeUserTokenExpired
+ @brief Indicates the token issue time is older than account's valid_since time.
+ */
+ FIRAuthInternalErrorCodeUserTokenExpired =
+ FIRAuthPublicErrorCodeFlag | FIRAuthErrorCodeUserTokenExpired,
+
+ /** @var FIRAuthInternalErrorCodeUserNotFound
+ @brief Indicates the user account was been found.
+ */
+ FIRAuthInternalErrorCodeUserNotFound =
+ FIRAuthPublicErrorCodeFlag | FIRAuthErrorCodeUserNotFound,
+
+ /** @var FIRAuthInternalErrorCodeInvalidAPIKey
+ @brief Indicates an invalid API Key was supplied in the request.
+ */
+ FIRAuthInternalErrorCodeInvalidAPIKey =
+ FIRAuthPublicErrorCodeFlag | FIRAuthErrorCodeInvalidAPIKey,
+
+ /** @var FIRAuthInternalErrorCodeOperationNotAllowed
+ @brief Indicates that admin disabled sign-in with the specified IDP.
+ */
+ FIRAuthInternalErrorCodeOperationNotAllowed =
+ FIRAuthPublicErrorCodeFlag | FIRAuthErrorCodeOperationNotAllowed,
+
+ /** @var FIRAuthInternalErrorCodeUserMismatch
+ @brief Indicates that user attempted to reauthenticate with a user other than the current
+ user.
+ */
+ FIRAuthInternalErrorCodeUserMismatch =
+ FIRAuthPublicErrorCodeFlag | FIRAuthErrorCodeUserMismatch,
+
+ /** @var FIRAuthInternalErrorCodeCredentialAlreadyInUse
+ @brief Indicates an attempt to link with a credential that has already been linked with a
+ different Firebase account.
+ */
+ FIRAuthInternalErrorCodeCredentialAlreadyInUse =
+ FIRAuthPublicErrorCodeFlag | FIRAuthErrorCodeCredentialAlreadyInUse,
+
+ /** @var FIRAuthInternalErrorCodeWeakPassword
+ @brief Indicates an attempt to set a password that is considered too weak.
+ */
+ FIRAuthInternalErrorCodeWeakPassword =
+ FIRAuthPublicErrorCodeFlag | FIRAuthErrorCodeWeakPassword,
+
+ /** @var FIRAuthInternalErrorCodeAppNotAuthorized
+ @brief Indicates the App is not authorized to use Firebase Authentication with the
+ provided API Key.
+ */
+ FIRAuthInternalErrorCodeAppNotAuthorized =
+ FIRAuthPublicErrorCodeFlag | FIRAuthErrorCodeAppNotAuthorized,
+
+ /** @var FIRAuthInternalErrorCodeExpiredActionCode
+ @brief Indicates the OOB code is expired.
+ */
+ FIRAuthInternalErrorCodeExpiredActionCode =
+ FIRAuthPublicErrorCodeFlag | FIRAuthErrorCodeExpiredActionCode,
+
+ /** @var FIRAuthInternalErrorCodeInvalidActionCode
+ @brief Indicates the OOB code is invalid.
+ */
+ FIRAuthInternalErrorCodeInvalidActionCode =
+ FIRAuthPublicErrorCodeFlag | FIRAuthErrorCodeInvalidActionCode,
+
+ /** Indicates that there are invalid parameters in the payload during a "send password reset email
+ * " attempt.
+ */
+ FIRAuthInternalErrorCodeInvalidMessagePayload =
+ FIRAuthPublicErrorCodeFlag | FIRAuthErrorCodeInvalidMessagePayload,
+
+ /** Indicates that the sender email is invalid during a "send password reset email" attempt.
+ */
+ FIRAuthInternalErrorCodeInvalidSender =
+ FIRAuthPublicErrorCodeFlag | FIRAuthErrorCodeInvalidSender,
+
+ /** Indicates that the recipient email is invalid.
+ */
+ FIRAuthInternalErrorCodeInvalidRecipientEmail =
+ FIRAuthPublicErrorCodeFlag | FIRAuthErrorCodeInvalidRecipientEmail,
+
+ /** Indicates that a phone number was not provided in a call to @c verifyPhoneNumber:completion:.
+ */
+ FIRAuthInternalErrorCodeMissingPhoneNumber =
+ FIRAuthPublicErrorCodeFlag | FIRAuthErrorCodeMissingPhoneNumber,
+
+ /** Indicates that an invalid phone number was provided in a call to @c
+ verifyPhoneNumber:completion:.
+ */
+ FIRAuthInternalErrorCodeInvalidPhoneNumber =
+ FIRAuthPublicErrorCodeFlag | FIRAuthErrorCodeInvalidPhoneNumber,
+
+ /** Indicates that the phone auth credential was created with an empty verification code.
+ */
+ FIRAuthInternalErrorCodeMissingVerificationCode =
+ FIRAuthPublicErrorCodeFlag | FIRAuthErrorCodeMissingVerificationCode,
+
+ /** Indicates that an invalid verification code was used in the verifyPhoneNumber request.
+ */
+ FIRAuthInternalErrorCodeInvalidVerificationCode =
+ FIRAuthPublicErrorCodeFlag | FIRAuthErrorCodeInvalidVerificationCode,
+
+ /** Indicates that the phone auth credential was created with an empty verification ID.
+ */
+ FIRAuthInternalErrorCodeMissingVerificationID =
+ FIRAuthPublicErrorCodeFlag | FIRAuthErrorCodeMissingVerificationID,
+
+ /** Indicates that the APNS device token is missing in the verifyClient request.
+ */
+ FIRAuthInternalErrorCodeMissingAppCredential =
+ FIRAuthPublicErrorCodeFlag | FIRAuthErrorCodeMissingAppCredential,
+
+ /** Indicates that an invalid APNS device token was used in the verifyClient request.
+ */
+ FIRAuthInternalErrorCodeInvalidAppCredential =
+ FIRAuthPublicErrorCodeFlag | FIRAuthErrorCodeInvalidAppCredential,
+
+ /** Indicates that an invalid verification ID was used in the verifyPhoneNumber request.
+ */
+ FIRAuthInternalErrorCodeInvalidVerificationID =
+ FIRAuthPublicErrorCodeFlag | FIRAuthErrorCodeInvalidVerificationID,
+
+ /** Indicates that the quota of SMS messages for a given project has been exceeded.
+ */
+ FIRAuthInternalErrorCodeQuotaExceeded =
+ FIRAuthPublicErrorCodeFlag | FIRAuthErrorCodeQuotaExceeded,
+
+ // The enum values between 17046 and 17051 are reserved and should NOT be used for new error
+ // codes.
+
+ /** Indicates that the SMS code has expired
+ */
+ FIRAuthInternalErrorCodeSessionExpired =
+ FIRAuthPublicErrorCodeFlag | FIRAuthErrorCodeSessionExpired,
+
+ FIRAuthInternalErrorCodeMissingAppToken =
+ FIRAuthPublicErrorCodeFlag | FIRAuthErrorCodeMissingAppToken,
+
+ FIRAuthInternalErrorCodeNotificationNotForwarded =
+ FIRAuthPublicErrorCodeFlag | FIRAuthErrorCodeNotificationNotForwarded,
+
+ FIRAuthInternalErrorCodeAppNotVerified =
+ FIRAuthPublicErrorCodeFlag | FIRAuthErrorCodeAppNotVerified,
+
+ /** @var FIRAuthInternalErrorCodeRPCRequestEncodingError
+ @brief Indicates an error encoding the RPC request.
+ @remarks This is typically due to some sort of unexpected input value.
+
+ See the @c NSUnderlyingError value in the @c NSError.userInfo dictionary for details.
+ */
+ FIRAuthInternalErrorCodeRPCRequestEncodingError = 1,
+
+ /** @var FIRAuthInternalErrorCodeJSONSerializationError
+ @brief Indicates an error serializing an RPC request.
+ @remarks This is typically due to some sort of unexpected input value.
+
+ If an @c NSJSONSerialization.isValidJSONObject: check fails, the error will contain no
+ @c NSUnderlyingError key in the @c NSError.userInfo dictionary. If an error was
+ encountered calling @c NSJSONSerialization.dataWithJSONObject:options:error:, the
+ resulting error will be associated with the @c NSUnderlyingError key in the
+ @c NSError.userInfo dictionary.
+ */
+ FIRAuthInternalErrorCodeJSONSerializationError = 2,
+
+ /** @var FIRAuthInternalErrorCodeUnexpectedErrorResponse
+ @brief Indicates an HTTP error occurred and the data returned either couldn't be deserialized
+ or couldn't be decoded.
+ @remarks See the @c NSUnderlyingError value in the @c NSError.userInfo dictionary for details
+ about the HTTP error which occurred.
+
+ If the response could be deserialized as JSON then the @c NSError.userInfo dictionary will
+ contain a value for the key @c FIRAuthErrorUserInfoDeserializedResponseKey which is the
+ deserialized response value.
+
+ If the response could not be deserialized as JSON then the @c NSError.userInfo dictionary
+ will contain values for the @c NSUnderlyingErrorKey and @c FIRAuthErrorUserInfoDataKey
+ keys.
+ */
+ FIRAuthInternalErrorCodeUnexpectedErrorResponse = 3,
+
+ /** @var FIRAuthInternalErrorCodeUnexpectedResponse
+ @brief Indicates the HTTP response indicated the request was a successes, but the response
+ contains something other than a JSON-encoded dictionary, or the data type of the response
+ indicated it is different from the type of response we expected.
+ @remarks See the @c NSUnderlyingError value in the @c NSError.userInfo dictionary.
+ If this key is present in the dictionary, it may contain an error from
+ @c NSJSONSerialization error (indicating the response received was of the wrong data
+ type).
+
+ See the @c FIRAuthErrorUserInfoDeserializedResponseKey value in the @c NSError.userInfo
+ dictionary. If the response could be deserialized, it's deserialized representation will
+ be associated with this key. If the @c NSUnderlyingError value in the @c NSError.userInfo
+ dictionary is @c nil, this indicates the JSON didn't represent a dictionary.
+ */
+ FIRAuthInternalErrorCodeUnexpectedResponse = 4,
+
+ /** @var FIRAuthInternalErrorCodeRPCResponseDecodingError
+ @brief Indicates an error decoding the RPC response.
+ This is typically due to some sort of unexpected response value from the server.
+ @remarks See the @c NSUnderlyingError value in the @c NSError.userInfo dictionary for details.
+
+ See the @c FIRErrorUserInfoDecodedResponseKey value in the @c NSError.userInfo dictionary.
+ The deserialized representation of the response will be associated with this key.
+ */
+ FIRAuthInternalErrorCodeRPCResponseDecodingError = 5,
+};
diff --git a/Firebase/Auth/Source/Private/FIRAuthKeychain.h b/Firebase/Auth/Source/Private/FIRAuthKeychain.h
new file mode 100644
index 0000000..c52e26a
--- /dev/null
+++ b/Firebase/Auth/Source/Private/FIRAuthKeychain.h
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ @brief The protocol for permanant data storage.
+ */
+@protocol FIRAuthStorage <NSObject>
+
+/** @fn initWithService:
+ @brief Initialize a @c FIRAuthStorage instance.
+ @param service The name of the storage service to use.
+ @return An initialized @c FIRAuthStorage instance for the specified service.
+ */
+- (id<FIRAuthStorage>)initWithService:(NSString *)service;
+
+/** @fn dataForKey:error:
+ @brief Gets the data for @c key in the storage. The key is set for the attribute
+ @c kSecAttrAccount of a generic password query.
+ @param key The key to use.
+ @param error The address to store any error that occurs during the process, if not NULL.
+ If the operation was successful, its content is set to @c nil .
+ @return The data stored in the storage for @c key, if any.
+ */
+- (nullable NSData *)dataForKey:(NSString *)key error:(NSError **_Nullable)error;
+
+/** @fn setData:forKey:error:
+ @brief Sets the data for @c key in the storage. The key is set for the attribute
+ @c kSecAttrAccount of a generic password query.
+ @param data The data to store.
+ @param key The key to use.
+ @param error The address to store any error that occurs during the process, if not NULL.
+ @return Whether the operation succeeded or not.
+ */
+- (BOOL)setData:(NSData *)data forKey:(NSString *)key error:(NSError **_Nullable)error;
+
+/** @fn removeDataForKey:error:
+ @brief Removes the data for @c key in the storage. The key is set for the attribute
+ @c kSecAttrAccount of a generic password query.
+ @param key The key to use.
+ @param error The address to store any error that occurs during the process, if not NULL.
+ @return Whether the operation succeeded or not.
+ */
+- (BOOL)removeDataForKey:(NSString *)key error:(NSError **_Nullable)error;
+
+@end
+
+/** @class FIRAuthKeychain
+ @brief The utility class to manipulate data in iOS Keychain.
+ */
+@interface FIRAuthKeychain : NSObject <FIRAuthStorage>
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/Private/FIRAuthNotificationManager.h b/Firebase/Auth/Source/Private/FIRAuthNotificationManager.h
new file mode 100644
index 0000000..42e5db8
--- /dev/null
+++ b/Firebase/Auth/Source/Private/FIRAuthNotificationManager.h
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+#import <UIKit/UIKit.h>
+
+@class FIRAuthAppCredentialManager;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @typedef FIRAuthNotificationForwardingCallback
+ @brief The type of block to receive whether or not remote notifications are being forwarded.
+ @param isNotificationBeingForwarded Whether or not remote notifications are being forwarded.
+ */
+typedef void (^FIRAuthNotificationForwardingCallback)(BOOL isNotificationBeingForwarded);
+
+/** @class FIRAuthNotificationManager
+ */
+@interface FIRAuthNotificationManager : NSObject
+
+/** @property timeout
+ @brief The timeout for checking for notification forwarding.
+ @remarks Only tests should access this property.
+ */
+@property(nonatomic, assign) NSTimeInterval timeout;
+
+/** @fn initWithApplication:appCredentialManager:
+ @brief Initializes the instance.
+ @param application The application.
+ @param appCredentialManager The object to handle app credentials delivered via notification.
+ @return The initialized instance.
+ */
+- (instancetype)initWithApplication:(UIApplication *)application
+ appCredentialManager:(FIRAuthAppCredentialManager *)appCredentialManager
+ NS_DESIGNATED_INITIALIZER;
+
+/** @fn init
+ @brief please use initWithAppCredentialManager: instead.
+ */
+- (instancetype)init NS_UNAVAILABLE;
+
+/** @fn checkNotificationForwardingWithCallback:
+ @brief Checks whether or not remote notifications are being forwarded to this class.
+ @param callback The block to be called either immediately or in future once a result
+ is available.
+ */
+- (void)checkNotificationForwardingWithCallback:(FIRAuthNotificationForwardingCallback)callback;
+
+/** @fn canHandleNotification:
+ @brief Attempts to handle the remote notification.
+ @param notification The notification in question.
+ @return Whether or the notification has been handled.
+ */
+- (BOOL)canHandleNotification:(NSDictionary *)notification;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/Private/FIRAuthSerialTaskQueue.h b/Firebase/Auth/Source/Private/FIRAuthSerialTaskQueue.h
new file mode 100644
index 0000000..cdae046
--- /dev/null
+++ b/Firebase/Auth/Source/Private/FIRAuthSerialTaskQueue.h
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @typedef FIRAuthSerialTaskCompletionBlock
+ @brief The type of method a @c FIRAuthSerialTask must call when it is complete.
+ */
+typedef void (^FIRAuthSerialTaskCompletionBlock)(void);
+
+/** @typedef FIRAuthSerialTask
+ @brief Represents a unit of work submitted to a task queue.
+ @param complete The task MUST call the complete method when done.
+ */
+typedef void (^FIRAuthSerialTask)(FIRAuthSerialTaskCompletionBlock complete);
+
+/** @class FIRAuthSerialTaskQueue
+ @brief An easy to use serial task queue which supports a callback-based completion notification
+ system for easy asyncronous call chaining.
+ */
+@interface FIRAuthSerialTaskQueue : NSObject
+
+/** @fn enqueueTask:
+ @brief Enqueues a task for serial execution in the queue.
+ @remarks The task MUST call the complete method when done. This method is thread-safe.
+ The task block won't be executed concurrently with any other blocks in other task queues or
+ the global work queue as returned by @c FIRAuthGlobalWorkQueue , but an uncompleted task
+ (e.g. task block finished executation before complete method is called at a later time)
+ does not affect other task queues or the global work queue.
+ */
+- (void)enqueueTask:(FIRAuthSerialTask)task;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/Private/FIRAuthUserDefaultsStorage.h b/Firebase/Auth/Source/Private/FIRAuthUserDefaultsStorage.h
new file mode 100644
index 0000000..13774ab
--- /dev/null
+++ b/Firebase/Auth/Source/Private/FIRAuthUserDefaultsStorage.h
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+// This class is only available in the simulator.
+#if TARGET_OS_SIMULATOR
+#ifndef FIRAUTH_USER_DEFAULTS_STORAGE_AVAILABLE
+#define FIRAUTH_USER_DEFAULTS_STORAGE_AVAILABLE 1
+#endif
+#endif
+
+#if FIRAUTH_USER_DEFAULTS_STORAGE_AVAILABLE
+
+#import "FIRAuthKeychain.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @class FIRAuthUserDefaultsStorage
+ @brief The utility class to storage data in NSUserDefaults.
+ */
+@interface FIRAuthUserDefaultsStorage : NSObject <FIRAuthStorage>
+
+/** @fn clear
+ @brief Clears all data from the storage.
+ @remarks This method is only supposed to be called from tests.
+ */
+- (void)clear;
+
+@end
+
+NS_ASSUME_NONNULL_END
+
+#endif // FIRAUTH_USER_DEFAULTS_STORAGE_AVAILABLE
diff --git a/Firebase/Auth/Source/Private/FIRAuth_Internal.h b/Firebase/Auth/Source/Private/FIRAuth_Internal.h
new file mode 100644
index 0000000..bdbefce
--- /dev/null
+++ b/Firebase/Auth/Source/Private/FIRAuth_Internal.h
@@ -0,0 +1,123 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRAuth.h"
+
+@class FIRAuthAPNSTokenManager;
+@class FIRAuthAppCredentialManager;
+@class FIRAuthNotificationManager;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @var FIRAuthStateDidChangeInternalNotification
+ @brief The name of the @c NSNotificationCenter notification which is posted when the auth state
+ changes (e.g. a new token has been produced, a user logs in or out). The object parameter of
+ the notification is a dictionary possibly containing the key:
+ @c FIRAuthStateDidChangeInternalNotificationTokenKey (the new access token.) If it does not
+ contain this key it indicates a sign-out event took place.
+ */
+extern NSString *const FIRAuthStateDidChangeInternalNotification;
+
+/** @var FIRAuthStateDidChangeInternalNotificationTokenKey
+ @brief A key present in the dictionary object parameter of the
+ @c FIRAuthStateDidChangeInternalNotification notification. The value associated with this
+ key will contain the new access token.
+ */
+extern NSString *const FIRAuthStateDidChangeInternalNotificationTokenKey;
+
+@interface FIRAuth ()
+
+/** @property APIKey
+ @brief The Google API key.
+ @remarks Needed for calls to identity toolkit and secure token service.
+ */
+@property(nonatomic, copy, readonly) NSString *APIKey;
+
+/** @property tokenManager
+ @brief The manager for APNs tokens used by phone number auth.
+ */
+@property(nonatomic, strong, readonly) FIRAuthAPNSTokenManager *tokenManager;
+
+/** @property appCredentailManager
+ @brief The manager for app credentials used by phone number auth.
+ */
+@property(nonatomic, strong, readonly) FIRAuthAppCredentialManager *appCredentialManager;
+
+/** @property notificationManager
+ @brief The manager for remote notifications used by phone number auth.
+ */
+@property(nonatomic, strong, readonly) FIRAuthNotificationManager *notificationManager;
+
+/** @fn initWithAPIKey:appName:
+ @brief Designated initializer.
+ @param APIKey The Google Developers Console API key for making requests from your app.
+ @param appName The name property of the previously created @c FIRApp instance.
+ */
+- (nullable instancetype)initWithAPIKey:(NSString *)APIKey
+ appName:(NSString *)appName NS_DESIGNATED_INITIALIZER;
+
+/** @fn notifyListenersOfAuthStateChange
+ @brief Posts the @c FIRAuthStateDidChangeNotification notification.
+ @remarks Called by @c FIRUser when token changes occur.
+ @param user The user whose tokens changed.
+ @param token The new access token associated with the user.
+ */
+- (void)notifyListenersOfAuthStateChangeWithUser:(nullable FIRUser *)user
+ token:(nullable NSString *)token;
+
+/** @fn updateKeychainWithUser:error:
+ @brief Updates the keychain for the given user.
+ @param user The user to be updated.
+ @param error The error caused the method to fail if the method returns NO.
+ @return Whether updating keychain has succeeded or not.
+ @remarks Called by @c FIRUser when user info or token changes occur.
+ */
+- (BOOL)updateKeychainWithUser:(FIRUser *)user error:(NSError *_Nullable *_Nullable)error;
+
+/** @fn internalSignInWithCredential:callback:
+ @brief Convenience method for @c internalSignInAndRetrieveDataWithCredential:callback:
+ This method doesn't return additional identity provider data.
+*/
+- (void)internalSignInWithCredential:(FIRAuthCredential *)credential
+ callback:(FIRAuthResultCallback)callback;
+
+/** @fn internalSignInAndRetrieveDataWithCredential:callback:
+ @brief Asynchronously signs in Firebase with the given 3rd party credentials (e.g. a Facebook
+ login Access Token, a Google ID Token/Access Token pair, etc.) and returns additional
+ identity provider data.
+ @param credential The credential supplied by the IdP.
+ @param isReauthentication Indicates whether or not the current invocation originated from an
+ attempt to reauthenticate.
+ @param callback A block which is invoked when the sign in finishes (or is cancelled.) Invoked
+ asynchronously on the auth global work queue in the future.
+ @remarks This is the internal counterpart of this method, which uses a callback that does not
+ update the current user.
+ */
+- (void)internalSignInAndRetrieveDataWithCredential:(FIRAuthCredential *)credential
+ isReauthentication:(BOOL)isReauthentication
+ callback:(nullable FIRAuthDataResultCallback)callback;
+
+/** @fn signOutByForceWithUserID:error:
+ @brief Signs out the current user.
+ @param userID The ID of the user to force sign out.
+ @param error An optional out parameter for error results.
+ @return @YES when the sign out request was successful. @NO otherwise.
+ */
+- (BOOL)signOutByForceWithUserID:(NSString *)userID error:(NSError *_Nullable *_Nullable)error;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/Private/FIRUser_Internal.h b/Firebase/Auth/Source/Private/FIRUser_Internal.h
new file mode 100644
index 0000000..1447607
--- /dev/null
+++ b/Firebase/Auth/Source/Private/FIRUser_Internal.h
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRUser.h"
+
+@class FIRAuth;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @typedef FIRRetrieveUserCallback
+ @brief The type of block that is invoked when the construction of a user succeeds or fails.
+ @param user The user that was constructed, or nil if user construction failed.
+ @param error The error which occurred, or nil if the request was successful.
+ */
+typedef void(^FIRRetrieveUserCallback)(FIRUser *_Nullable user, NSError *_Nullable error);
+
+@interface FIRUser () <NSSecureCoding>
+
+/** @property rawAccessToken
+ @brief The cached access token.
+ @remarks This method is specifically for providing the access token to internal clients during
+ deserialization and sign-in events, and should not be used to retrieve the access token by
+ anyone else.
+ */
+@property(nonatomic, copy, readonly) NSString *rawAccessToken;
+
+/** @property auth
+ @brief A weak reference to a FIRAuth instance which is used to notify auth of token changes.
+ */
+@property(nonatomic, weak) FIRAuth *auth;
+
+/** @var accessTokenExpirationDate
+ @brief The expiration date of the cached access token.
+ */
+@property(nonatomic, copy, readonly) NSDate *accessTokenExpirationDate;
+
+/** @fn retrieveUserWithAPIKey:accessToken:accessTokenExpirationDate:refreshToken:callback:
+ @brief Constructs a user with Secure Token Service tokens, and obtains user details from the
+ getAccountInfo endpoint.
+ @param APIKey The client API key for making RPCs.
+ @param accessToken The Secure Token Service access token.
+ @param accessTokenExpirationDate The approximate expiration date of the access token.
+ @param refreshToken The Secure Token Service refresh token.
+ @param anonymous Whether or not the user is anonymous.
+ @param callback A block which is invoked when the construction succeeds or fails. Invoked
+ asynchronously on the auth global work queue in the future.
+ */
++ (void)retrieveUserWithAPIKey:(NSString *)APIKey
+ accessToken:(NSString *)accessToken
+ accessTokenExpirationDate:(NSDate *)accessTokenExpirationDate
+ refreshToken:(NSString *)refreshToken
+ anonymous:(BOOL)anonymous
+ callback:(FIRRetrieveUserCallback)callback;
+
+/** @fn internalGetTokenForcingRefresh:callback:
+ @brief Retrieves the Firebase authentication token, possibly refreshing it if it has expired.
+ @param forceRefresh Forces a token refresh. Useful if the token becomes invalid for some reason
+ other than an expiration.
+ @param callback The block to invoke when the token is available. Invoked asynchronously on the
+ global work thread in the future.
+ */
+- (void)internalGetTokenForcingRefresh:(BOOL)forceRefresh
+ callback:(nonnull FIRAuthTokenCallback)callback;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/RPCs/FIRAuthBackend.h b/Firebase/Auth/Source/RPCs/FIRAuthBackend.h
new file mode 100644
index 0000000..519a6e7
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRAuthBackend.h
@@ -0,0 +1,496 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+@class FIRCreateAuthURIRequest;
+@class FIRCreateAuthURIResponse;
+@class FIRGetAccountInfoRequest;
+@class FIRGetAccountInfoResponse;
+@class FIRGetOOBConfirmationCodeRequest;
+@class FIRGetOOBConfirmationCodeResponse;
+@class FIRResetPasswordRequest;
+@class FIRResetPasswordResponse;
+@class FIRSecureTokenRequest;
+@class FIRSecureTokenResponse;
+@class FIRSetAccountInfoRequest;
+@class FIRSetAccountInfoResponse;
+@class FIRVerifyAssertionRequest;
+@class FIRVerifyAssertionResponse;
+@class FIRVerifyClientRequest;
+@class FIRVerifyClientResponse;
+@class FIRVerifyCustomTokenRequest;
+@class FIRVerifyCustomTokenResponse;
+@class FIRVerifyPasswordRequest;
+@class FIRVerifyPasswordResponse;
+@class FIRVerifyPhoneNumberRequest;
+@class FIRVerifyPhoneNumberResponse;
+@class FIRSendVerificationCodeRequest;
+@class FIRSendVerificationCodeResponse;
+@class FIRSignUpNewUserRequest;
+@class FIRSignUpNewUserResponse;
+@class FIRDeleteAccountRequest;
+@class FIRDeleteAccountResponse;
+@protocol FIRAuthBackendImplementation;
+@protocol FIRAuthBackendRPCIssuer;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @typedef FIRAuthBackendRPCIssuerCompletionHandler
+ @brief The type of block used to return the result of a call to an endpoint.
+ @param data The HTTP response body.
+ @param error The error which occurred, if any.
+ @remarks One of response or error will be non-nil.
+ */
+typedef void (^FIRAuthBackendRPCIssuerCompletionHandler)(NSData *_Nullable data,
+ NSError *_Nullable error);
+
+/** @typedef FIRCreateAuthURIResponseCallback
+ @brief The type of block used to return the result of a call to the createAuthURI
+ endpoint.
+ @param response The received response, if any.
+ @param error The error which occurred, if any.
+ @remarks One of response or error will be non-nil.
+ */
+typedef void (^FIRCreateAuthURIResponseCallback)
+ (FIRCreateAuthURIResponse *_Nullable response, NSError *_Nullable error);
+
+/** @typedef FIRGetAccountInfoResponseCallback
+ @brief The type of block used to return the result of a call to the getAccountInfo
+ endpoint.
+ @param response The received response, if any.
+ @param error The error which occurred, if any.
+ @remarks One of response or error will be non-nil.
+ */
+typedef void (^FIRGetAccountInfoResponseCallback)
+ (FIRGetAccountInfoResponse *_Nullable response, NSError *_Nullable error);
+
+/** @typedef FIRSetAccountInfoResponseCallback
+ @brief The type of block used to return the result of a call to the setAccountInfo
+ endpoint.
+ @param response The received response, if any.
+ @param error The error which occurred, if any.
+ @remarks One of response or error will be non-nil.
+ */
+typedef void (^FIRSetAccountInfoResponseCallback)
+ (FIRSetAccountInfoResponse *_Nullable response, NSError *_Nullable error);
+
+/** @typedef FIRSecureTokenResponseCallback
+ @brief The type of block used to return the result of a call to the token endpoint.
+ @param response The received response, if any.
+ @param error The error which occurred, if any.
+ @remarks One of response or error will be non-nil.
+ */
+typedef void (^FIRSecureTokenResponseCallback)
+ (FIRSecureTokenResponse *_Nullable response, NSError *_Nullable error);
+
+/** @typedef FIRVerifyAssertionResponseCallback
+ @brief The type of block used to return the result of a call to the verifyAssertion
+ endpoint.
+ @param response The received response, if any.
+ @param error The error which occurred, if any.
+ @remarks One of response or error will be non-nil.
+ */
+typedef void (^FIRVerifyAssertionResponseCallback)
+ (FIRVerifyAssertionResponse *_Nullable response, NSError *_Nullable error);
+
+/** @typedef FIRVerifyPasswordResponseCallback
+ @brief The type of block used to return the result of a call to the verifyPassword
+ endpoint.
+ @param response The received response, if any.
+ @param error The error which occurred, if any.
+ @remarks One of response or error will be non-nil.
+ */
+typedef void (^FIRVerifyPasswordResponseCallback)
+ (FIRVerifyPasswordResponse *_Nullable response, NSError *_Nullable error);
+
+/** @typedef FIRVerifyCustomTokenResponseCallback
+ @brief The type of block used to return the result of a call to the verifyCustomToken
+ endpoint.
+ @param response The received response, if any.
+ @param error The error which occurred, if any.
+ @remarks One of response or error will be non-nil.
+ */
+typedef void (^FIRVerifyCustomTokenResponseCallback)
+ (FIRVerifyCustomTokenResponse *_Nullable response, NSError *_Nullable error);
+
+/** @typedef FIRDeleteCallBack
+ @brief The type of block called when a request delete account has finished.
+ @param error The error which occurred, or nil if the request was successful.
+ */
+typedef void (^FIRDeleteCallBack)(NSError *_Nullable error);
+
+/** @typedef FIRGetOOBConfirmationCodeResponseCallback
+ @brief The type of block used to return the result of a call to the getOOBConfirmationCode
+ endpoint.
+ @param response The received response, if any.
+ @param error The error which occurred, if any.
+ @remarks One of response or error will be non-nil.
+ */
+typedef void (^FIRGetOOBConfirmationCodeResponseCallback)
+ (FIRGetOOBConfirmationCodeResponse *_Nullable response, NSError *_Nullable error);
+
+/** @typedef FIRSignupNewUserCallback
+ @brief The type of block used to return the result of a call to the signupNewUser endpoint.
+ @param response The received response, if any.
+ @param error The error which occurred, if any.
+ @remarks One of response or error will be non-nil.
+ */
+typedef void (^FIRSignupNewUserCallback)
+ (FIRSignUpNewUserResponse *_Nullable response, NSError *_Nullable error);
+
+/** @typedef FIRResetPasswordCallback
+ @brief The type of block used to return the result of a call to the resetPassword endpoint.
+ @param response The received response, if any.
+ @param error The error which occurred, if any.
+ @remarks One of response or error will be non-nil.
+ */
+typedef void (^FIRResetPasswordCallback)
+ (FIRResetPasswordResponse *_Nullable response, NSError *_Nullable error);
+
+/** @typedef FIRSendVerificationCodeResponseCallback
+ @brief The type of block used to return the result of a call to the sendVerificationCode
+ endpoint.
+ @param response The received response, if any.
+ @param error The error which occurred, if any.
+ @remarks One of response or error will be non-nil.
+ */
+typedef void (^FIRSendVerificationCodeResponseCallback)
+ (FIRSendVerificationCodeResponse *_Nullable response, NSError *_Nullable error);
+
+/** @typedef FIRVerifyPhoneNumberResponseCallback
+ @brief The type of block used to return the result of a call to the verifyPhoneNumber endpoint.
+ @param response The received response, if any.
+ @param error The error which occurred, if any.
+ @remarks One of response or error will be non-nil.
+ */
+typedef void (^FIRVerifyPhoneNumberResponseCallback)
+ (FIRVerifyPhoneNumberResponse *_Nullable response, NSError *_Nullable error);
+
+/** @typedef FIRVerifyClientResponseCallback
+ @brief The type of block used to return the result of a call to the verifyClient endpoint.
+ @param response The received response, if any.
+ @param error The error which occurred, if any.
+ @remarks One of response or error will be non-nil.
+ */
+typedef void (^FIRVerifyClientResponseCallback)
+ (FIRVerifyClientResponse *_Nullable response, NSError *_Nullable error);
+
+/** @class FIRAuthBackend
+ @brief Simple static class with methods representing the backend RPCs.
+ @remarks All callback blocks passed as method parameters are invoked asynchronously on the
+ global work queue in the future. See
+ https://github.com/firebase/firebase-ios-sdk/tree/master/Firebase/Auth/Docs/threading.ml
+ */
+@interface FIRAuthBackend : NSObject
+
+/** @fn setBackendImplementation:
+ @brief Changes the default backend implementation to something else.
+ @param backendImplementation The backend implementation to use.
+ @remarks This is not, generally, safe to call in a scenario where other backend requests may
+ be occuring. This is specifically to help mock the backend for testing purposes.
+ */
++ (void)setBackendImplementation:(id<FIRAuthBackendImplementation>)backendImplementation;
+
+/** @fn setDefaultBackendImplementationWithRPCIssuer:
+ @brief Uses the default backend implementation, but with a custom RPC issuer.
+ @param RPCIssuer The RPC issuer to use. If @c nil, will use the default implementation.
+ @remarks This is not, generally, safe to call in a scenario where other backend requests may
+ be occuring. This is specifically to help test the backend interfaces (requests, responses,
+ and shared FIRAuthBackend logic.)
+ */
++ (void)setDefaultBackendImplementationWithRPCIssuer:
+ (nullable id<FIRAuthBackendRPCIssuer>)RPCIssuer;
+
+/** @fn createAuthURI:callback:
+ @brief Calls the createAuthURI endpoint, which is responsible for creating the URI used by the
+ IdP to authenticate the user.
+ @param request The request parameters.
+ @param callback The callback.
+ */
++ (void)createAuthURI:(FIRCreateAuthURIRequest *)request
+ callback:(FIRCreateAuthURIResponseCallback)callback;
+
+/** @fn getAccountInfo:callback:
+ @brief Calls the getAccountInfo endpoint, which returns account info for a given account.
+ @param request The request parameters.
+ @param callback The callback.
+ */
++ (void)getAccountInfo:(FIRGetAccountInfoRequest *)request
+ callback:(FIRGetAccountInfoResponseCallback)callback;
+
+/** @fn setAccountInfo:callback:
+ @brief Calls the setAccountInfo endpoint, which is responsible for setting account info for a
+ user, for example, to sign up a new user with email and password.
+ @param request The request parameters.
+ @param callback The callback.
+ */
++ (void)setAccountInfo:(FIRSetAccountInfoRequest *)request
+ callback:(FIRSetAccountInfoResponseCallback)callback;
+
+/** @fn verifyAssertion:callback:
+ @brief Calls the verifyAssertion endpoint, which is responsible for authenticating a
+ user who has IDP-related credentials (an ID Token, an Access Token, etc.)
+ @param request The request parameters.
+ @param callback The callback.
+ */
++ (void)verifyAssertion:(FIRVerifyAssertionRequest *)request
+ callback:(FIRVerifyAssertionResponseCallback)callback;
+
+/** @fn verifyCustomToken:callback:
+ @brief Calls the verifyCustomToken endpoint, which is responsible for authenticating a
+ user who has BYOAuth credentials (a self-signed token using their BYOAuth private key.)
+ @param request The request parameters.
+ @param callback The callback.
+ */
++ (void)verifyCustomToken:(FIRVerifyCustomTokenRequest *)request
+ callback:(FIRVerifyCustomTokenResponseCallback)callback;
+
+/** @fn verifyPassword:callback:
+ @brief Calls the verifyPassword endpoint, which is responsible for authenticating a
+ user who has email and password credentials.
+ @param request The request parameters.
+ @param callback The callback.
+ */
++ (void)verifyPassword:(FIRVerifyPasswordRequest *)request
+ callback:(FIRVerifyPasswordResponseCallback)callback;
+
+/** @fn secureToken:callback:
+ @brief Calls the token endpoint, which is responsible for performing STS token exchanges and
+ token refreshes.
+ @param request The request parameters.
+ @param callback The callback.
+ */
++ (void)secureToken:(FIRSecureTokenRequest *)request
+ callback:(FIRSecureTokenResponseCallback)callback;
+
+/** @fn getOOBConfirmationCode:callback:
+ @brief Calls the getOOBConfirmationCode endpoint, which is responsible for sending email change
+ request emails, and password reset emails.
+ @param request The request parameters.
+ @param callback The callback.
+ */
++ (void)getOOBConfirmationCode:(FIRGetOOBConfirmationCodeRequest *)request
+ callback:(FIRGetOOBConfirmationCodeResponseCallback)callback;
+
+/** @fn signUpNewUser:
+ @brief Calls the signUpNewUser endpoint, which is responsible anonymously signing up a user
+ or signing in a user anonymously.
+ @param request The request parameters.
+ @param callback The callback.
+ */
++ (void)signUpNewUser:(FIRSignUpNewUserRequest *)request
+ callback:(FIRSignupNewUserCallback)callback;
+
+/** @fn resetPassword:callback
+ @brief Calls the resetPassword endpoint, which is responsible for resetting a user's password
+ given an OOB code and new password.
+ @param request The request parameters.
+ @param callback The callback.
+ */
++ (void)resetPassword:(FIRResetPasswordRequest *)request
+ callback:(FIRResetPasswordCallback)callback;
+
+/** @fn deleteAccount:
+ @brief Calls the DeleteAccount endpoint, which is responsible for deleting a user.
+ @param request The request parameters.
+ @param callback The callback.
+ */
++ (void)deleteAccount:(FIRDeleteAccountRequest *)request
+ callback:(FIRDeleteCallBack)callback;
+
+/** @fn sendVerificationCode:callback:
+ @brief Calls the sendVerificationCode endpoint, which is responsible for sending the
+ verification code to a phone number specified in the request parameters.
+ @param request The request parameters.
+ @param callback The callback.
+ */
++ (void)sendVerificationCode:(FIRSendVerificationCodeRequest *)request
+ callback:(FIRSendVerificationCodeResponseCallback)callback;
+
+/** @fn verifyPhoneNumber:callback:
+ @brief Calls the verifyPhoneNumber endpoint, which is responsible for sending the verification
+ code to a phone number specified in the request parameters.
+ @param request The request parameters.
+ @param callback The callback.
+ */
++ (void)verifyPhoneNumber:(FIRVerifyPhoneNumberRequest *)request
+ callback:(FIRVerifyPhoneNumberResponseCallback)callback;
+
+/** @fn verifyClient:callback:
+ @brief Calls the verifyClient endpoint, which is responsible for sending the silent push
+ notification used for app validation to the device provided in the request parameters.
+ @param request The request parameters.
+ @param callback The callback.
+ */
++ (void)verifyClient:(FIRVerifyClientRequest *)request
+ callback:(FIRVerifyClientResponseCallback)callback;
+
+@end
+
+/** @protocol FIRAuthBackendRPCIssuer
+ @brief Used to make FIRAuthBackend
+ */
+@protocol FIRAuthBackendRPCIssuer <NSObject>
+
+/** @fn asyncPostToURL:body:contentType:completionHandler:
+ @brief Asynchronously sends a POST request.
+ @param URL URL of the request.
+ @param body Request body.
+ @param contentType Content type of the body.
+ @param handler provided that handles POST response. Invoked asynchronously on the auth global
+ work queue in the future.
+ */
+- (void)asyncPostToURL:(NSURL *)URL
+ body:(NSData *)body
+ contentType:(NSString *)contentType
+ completionHandler:(FIRAuthBackendRPCIssuerCompletionHandler)handler;
+
+@end
+
+/** @protocol FIRAuthBackendImplementation
+ @brief Used to make FIRAuthBackend provide a layer of indirection to an actual RPC-based backend
+ or a mock backend.
+ */
+@protocol FIRAuthBackendImplementation <NSObject>
+
+/** @fn createAuthURI:callback:
+ @brief Calls the createAuthURI endpoint, which is responsible for creating the URI used by the
+ IdP to authenticate the user.
+ @param request The request parameters.
+ @param callback The callback.
+ */
+- (void)createAuthURI:(FIRCreateAuthURIRequest *)request
+ callback:(FIRCreateAuthURIResponseCallback)callback;
+
+/** @fn getAccountInfo:callback:
+ @brief Calls the getAccountInfo endpoint, which returns account info for a given account.
+ @param request The request parameters.
+ @param callback The callback.
+ */
+- (void)getAccountInfo:(FIRGetAccountInfoRequest *)request
+ callback:(FIRGetAccountInfoResponseCallback)callback;
+
+/** @fn setAccountInfo:callback:
+ @brief Calls the setAccountInfo endpoint, which is responsible for setting account info for a
+ user, for example, to sign up a new user with email and password.
+ @param request The request parameters.
+ @param callback The callback.
+ */
+- (void)setAccountInfo:(FIRSetAccountInfoRequest *)request
+ callback:(FIRSetAccountInfoResponseCallback)callback;
+
+/** @fn verifyAssertion:callback:
+ @brief Calls the verifyAssertion endpoint, which is responsible for authenticating a
+ user who has IDP-related credentials (an ID Token, an Access Token, etc.)
+ @param request The request parameters.
+ @param callback The callback.
+ */
+- (void)verifyAssertion:(FIRVerifyAssertionRequest *)request
+ callback:(FIRVerifyAssertionResponseCallback)callback;
+
+/** @fn verifyCustomToken:callback:
+ @brief Calls the verifyCustomToken endpoint, which is responsible for authenticating a
+ user who has BYOAuth credentials (a self-signed token using their BYOAuth private key.)
+ @param request The request parameters.
+ @param callback The callback.
+ */
+- (void)verifyCustomToken:(FIRVerifyCustomTokenRequest *)request
+ callback:(FIRVerifyCustomTokenResponseCallback)callback;
+
+/** @fn verifyPassword:callback:
+ @brief Calls the verifyPassword endpoint, which is responsible for authenticating a
+ user who has email and password credentials.
+ @param request The request parameters.
+ @param callback The callback.
+ */
+- (void)verifyPassword:(FIRVerifyPasswordRequest *)request
+ callback:(FIRVerifyPasswordResponseCallback)callback;
+
+/** @fn secureToken:callback:
+ @brief Calls the token endpoint, which is responsible for performing STS token exchanges and
+ token refreshes.
+ @param request The request parameters.
+ @param callback The callback.
+ */
+- (void)secureToken:(FIRSecureTokenRequest *)request
+ callback:(FIRSecureTokenResponseCallback)callback;
+
+/** @fn getOOBConfirmationCode:callback:
+ @brief Calls the getOOBConfirmationCode endpoint, which is responsible for sending email change
+ request emails, and password reset emails.
+ @param request The request parameters.
+ @param callback The callback.
+ */
+- (void)getOOBConfirmationCode:(FIRGetOOBConfirmationCodeRequest *)request
+ callback:(FIRGetOOBConfirmationCodeResponseCallback)callback;
+
+/** @fn signUpNewUser:
+ @brief Calls the signUpNewUser endpoint, which is responsible anonymously signing up a user
+ or signing in a user anonymously.
+ @param request The request parameters.
+ @param callback The callback.
+ */
+- (void)signUpNewUser:(FIRSignUpNewUserRequest *)request
+ callback:(FIRSignupNewUserCallback)callback;
+
+/** @fn deleteAccount:
+ @brief Calls the DeleteAccount endpoint, which is responsible for deleting a user.
+ @param request The request parameters.
+ @param callback The callback.
+ */
+- (void)deleteAccount:(FIRDeleteAccountRequest *)request
+ callback:(FIRDeleteCallBack)callback;
+
+/** @fn sendVerificationCode:callback:
+ @brief Calls the sendVerificationCode endpoint, which is responsible for sending the
+ verification code to a phone number specified in the request parameters.
+ @param request The request parameters.
+ @param callback The callback.
+ */
+- (void)sendVerificationCode:(FIRSendVerificationCodeRequest *)request
+ callback:(FIRSendVerificationCodeResponseCallback)callback;
+
+/** @fn verifyPhoneNumber:callback:
+ @brief Calls the verifyPhoneNumber endpoint, which is responsible for sending the verification
+ code to a phone number specified in the request parameters.
+ @param request The request parameters.
+ @param callback The callback.
+ */
+- (void)verifyPhoneNumber:(FIRVerifyPhoneNumberRequest *)request
+ callback:(FIRVerifyPhoneNumberResponseCallback)callback;
+
+/** @fn verifyClient:callback:
+ @brief Calls the verifyClient endpoint, which is responsible for sending the silent push
+ notification used for app validation to the device provided in the request parameters.
+ @param request The request parameters.
+ @param callback The callback.
+ */
+- (void)verifyClient:(FIRVerifyClientRequest *)request
+ callback:(FIRVerifyClientResponseCallback)callback;
+
+/** @fn resetPassword:callback
+ @brief Calls the resetPassword endpoint, which is responsible for resetting a user's password
+ given an OOB code and new password.
+ @param request The request parameters.
+ @param callback The callback.
+ */
+- (void)resetPassword:(FIRResetPasswordRequest *)request
+ callback:(FIRResetPasswordCallback)callback;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/RPCs/FIRAuthBackend.m b/Firebase/Auth/Source/RPCs/FIRAuthBackend.m
new file mode 100644
index 0000000..3efc602
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRAuthBackend.m
@@ -0,0 +1,943 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRAuthBackend.h"
+
+#import "../AuthProviders/Phone/FIRPhoneAuthCredential_Internal.h"
+#import "../AuthProviders/Phone/FIRPhoneAuthProvider.h"
+#import "../Private/FIRAuthErrorUtils.h"
+#import "../Private/FIRAuthGlobalWorkQueue.h"
+#import "FirebaseAuth.h"
+#import "FIRAuthRPCRequest.h"
+#import "FIRAuthRPCResponse.h"
+#import "FIRCreateAuthURIRequest.h"
+#import "FIRCreateAuthURIResponse.h"
+#import "FIRDeleteAccountRequest.h"
+#import "FIRDeleteAccountResponse.h"
+#import "FIRGetAccountInfoRequest.h"
+#import "FIRGetAccountInfoResponse.h"
+#import "FIRGetOOBConfirmationCodeRequest.h"
+#import "FIRGetOOBConfirmationCodeResponse.h"
+#import "FIRResetPasswordRequest.h"
+#import "FIRResetPasswordResponse.h"
+#import "FIRSendVerificationCodeRequest.h"
+#import "FIRSendVerificationCodeResponse.h"
+#import "FIRSecureTokenRequest.h"
+#import "FIRSecureTokenResponse.h"
+#import "FIRSetAccountInfoRequest.h"
+#import "FIRSetAccountInfoResponse.h"
+#import "FIRSignUpNewUserRequest.h"
+#import "FIRSignUpNewUserResponse.h"
+#import "FIRVerifyAssertionRequest.h"
+#import "FIRVerifyAssertionResponse.h"
+#import "FIRVerifyClientRequest.h"
+#import "FIRVerifyClientResponse.h"
+#import "FIRVerifyCustomTokenRequest.h"
+#import "FIRVerifyCustomTokenResponse.h"
+#import "FIRVerifyPasswordRequest.h"
+#import "FIRVerifyPasswordResponse.h"
+#import "FIRVerifyPhoneNumberRequest.h"
+#import "FIRVerifyPhoneNumberResponse.h"
+#import <GTMSessionFetcher/GTMSessionFetcher.h>
+#import <GTMSessionFetcher/GTMSessionFetcherService.h>
+
+/** @var kIosBundleIdentifierHeader
+ @brief HTTP header name for iOS bundle ID.
+ */
+static NSString *const kIosBundleIdentifierHeader = @"X-Ios-Bundle-Identifier";
+
+/** @var kJSONContentType
+ @brief The value of the HTTP content-type header for JSON payloads.
+ */
+static NSString *const kJSONContentType = @"application/json";
+
+/** @var kErrorDataKey
+ @brief Key for error data in NSError returned by @c GTMSessionFetcher.
+ */
+static NSString * const kErrorDataKey = @"data";
+
+/** @var kErrorKey
+ @brief The key for the "error" value in JSON responses from the server.
+ */
+static NSString *const kErrorKey = @"error";
+
+/** @var kErrorsKey
+ @brief The key for the "errors" value in JSON responses from the server.
+ */
+static NSString *const kErrorsKey = @"errors";
+
+/** @var kReasonKey
+ @brief The key for the "reason" value in JSON responses from the server.
+ */
+static NSString *const kReasonKey = @"reason";
+
+/** @var kInvalidKeyReasonValue
+ @brief The value for the "reason" key indicating an invalid API Key was received by the server.
+ */
+static NSString *const kInvalidKeyReasonValue = @"keyInvalid";
+
+/** @var kAppNotAuthorizedReasonValue
+ @brief The value for the "reason" key indicating the App is not authorized to use Firebase
+ Authentication.
+ */
+static NSString *const kAppNotAuthorizedReasonValue = @"ipRefererBlocked";
+
+/** @var kErrorMessageKey
+ @brief The key for an error's "message" value in JSON responses from the server.
+ */
+static NSString *const kErrorMessageKey = @"message";
+
+/** @var kUserNotFoundErrorMessage
+ @brief This is the error message returned when the user is not found, which means the user
+ account has been deleted given the token was once valid.
+ */
+static NSString *const kUserNotFoundErrorMessage = @"USER_NOT_FOUND";
+
+/** @var kUserDeletedErrorMessage
+ @brief This is the error message the server will respond with if the user entered an invalid
+ email address.
+ */
+static NSString *const kUserDeletedErrorMessage = @"EMAIL_NOT_FOUND";
+
+/** @var kInvalidLocalIDErrorMessage
+ @brief This is the error message the server responds with if the user local id in the id token
+ does not exit.
+ */
+static NSString *const kInvalidLocalIDErrorMessage = @"INVALID_LOCAL_ID";
+
+/** @var kUserTokenExpiredErrorMessage
+ @brief The error returned by the server if the token issue time is older than the account's
+ valid_since time.
+ */
+static NSString *const kUserTokenExpiredErrorMessage = @"TOKEN_EXPIRED";
+
+/** @var kTooManyRequestsErrorMessage
+ @brief This is the error message the server will respond with if too many requests were made to
+ a server method.
+ */
+static NSString *const kTooManyRequestsErrorMessage = @"TOO_MANY_ATTEMPTS_TRY_LATER";
+
+/** @var kInvalidTokenCustomErrorMessage
+ @brief This is the error message the server will respond with if there is a validation error
+ with the custom token.
+ */
+static NSString *const kInvalidCustomTokenErrorMessage = @"INVALID_CUSTOM_TOKEN";
+
+/** @var kCustomTokenMismatch
+ @brief This is the error message the server will respond with if the service account and API key
+ belong to different projects.
+ */
+static NSString *const kCustomTokenMismatch = @"CREDENTIAL_MISMATCH";
+
+/** @var kInvalidCredentialErrorMessage
+ @brief This is the error message the server responds with if the IDP token or requestUri is
+ invalid.
+ */
+static NSString *const kInvalidCredentialErrorMessage = @"INVALID_IDP_RESPONSE";
+
+/** @var kUserDisabledErrorMessage
+ @brief The error returned by the server if the user account is diabled.
+ */
+static NSString *const kUserDisabledErrorMessage = @"USER_DISABLED";
+
+/** @var kOperationNotAllowedErrorMessage
+ @brief This is the error message the server will respond with if Admin disables IDP specified by
+ provider.
+ */
+static NSString *const kOperationNotAllowedErrorMessage = @"OPERATION_NOT_ALLOWED";
+
+/** @var kPasswordLoginDisabledErrorMessage
+ @brief This is the error message the server responds with if password login is disabled.
+ */
+static NSString *const kPasswordLoginDisabledErrorMessage = @"PASSWORD_LOGIN_DISABLED";
+
+/** @var kEmailAlreadyInUseErrorMessage
+ @brief This is the error message the server responds with if the email address already exists.
+ */
+static NSString *const kEmailAlreadyInUseErrorMessage = @"EMAIL_EXISTS";
+
+/** @var kInvalidEmailErrorMessage
+ @brief The error returned by the server if the email is invalid.
+ */
+static NSString *const kInvalidEmailErrorMessage = @"INVALID_EMAIL";
+
+/** @var kInvalidIdentifierErrorMessage
+ @brief The error returned by the server if the identifier is invalid.
+ */
+static NSString *const kInvalidIdentifierErrorMessage = @"INVALID_IDENTIFIER";
+
+/** @var kWrongPasswordErrorMessage
+ @brief This is the error message the server will respond with if the user entered a wrong
+ password.
+ */
+static NSString *const kWrongPasswordErrorMessage = @"INVALID_PASSWORD";
+
+/** @var kCredentialTooOldErrorMessage
+ @brief This is the error message the server responds with if account change is attempted 5
+ minutes after signing in.
+ */
+static NSString *const kCredentialTooOldErrorMessage = @"CREDENTIAL_TOO_OLD_LOGIN_AGAIN";
+
+/** @var kFederatedUserIDAlreadyLinkedMessage
+ @brief This is the error message the server will respond with if the federated user ID has been
+ already linked with another account.
+ */
+static NSString *const kFederatedUserIDAlreadyLinkedMessage = @"FEDERATED_USER_ID_ALREADY_LINKED";
+
+/** @var kInvalidUserTokenErrorMessage
+ @brief This is the error message the server responds with if user's saved auth credential is
+ invalid, and the user needs to sign in again.
+ */
+static NSString *const kInvalidUserTokenErrorMessage = @"INVALID_ID_TOKEN";
+
+/** @var kWeakPasswordErrorMessagePrefix
+ @brief This is the prefix for the error message the server responds with if user's new password
+ to be set is too weak.
+ */
+static NSString *const kWeakPasswordErrorMessagePrefix = @"WEAK_PASSWORD";
+
+/** @var kExpiredActionCodeErrorMessage
+ @brief This is the error message the server will respond with if the action code is expired.
+ */
+static NSString *const kExpiredActionCodeErrorMessage = @"EXPIRED_OOB_CODE";
+
+/** @var kInvalidActionCodeErrorMessage
+ @brief This is the error message the server will respond with if the action code is invalid.
+ */
+static NSString *const kInvalidActionCodeErrorMessage = @"INVALID_OOB_CODE";
+
+/** @var kInvalidSenderEmailErrorMessage
+ @brief This is the error message the server will respond with if the sender email is invalid
+ during a "send password reset email" attempt.
+ */
+static NSString *const kInvalidSenderEmailErrorMessage = @"INVALID_SENDER";
+
+/** @var kInvalidMessagePayloadErrorMessage
+ @brief This is the error message the server will respond with if there are invalid parameters in
+ the payload during a "send password reset email" attempt.
+ */
+static NSString *const kInvalidMessagePayloadErrorMessage = @"INVALID_MESSAGE_PAYLOAD";
+
+/** @var kInvalidRecipientEmailErrorMessage
+ @brief This is the error message the server will respond with if the recipient email is invalid.
+ */
+static NSString *const kInvalidRecipientEmailErrorMessage = @"INVALID_RECIPIENT_EMAIL";
+
+/** @var kInvalidPhoneNumberErrorMessage
+ @brief This is the error message the server will respond with if an incorrectly formatted phone
+ number is provided.
+ */
+static NSString *const kInvalidPhoneNumberErrorMessage = @"INVALID_PHONE_NUMBER";
+
+/** @var kInvalidVerificationCodeErrorMessage
+ @brief This is the error message the server will respond with if an invalid verification code is
+ provided.
+ */
+static NSString *const kInvalidVerificationCodeErrorMessage = @"INVALID_CODE";
+
+/** @var kInvalidSessionInfoErrorMessage
+ @brief This is the error message the server will respond with if an invalid session info
+ (verification ID) is provided.
+ */
+static NSString *const kInvalidSessionInfoErrorMessage = @"INVALID_SESSION_INFO";
+
+/** @var kSessionExpiredErrorMessage
+ @brief This is the error message the server will respond with if the SMS code has expired before
+ it is used.
+ */
+static NSString *const kSessionExpiredErrorMessage = @"SESSION_EXPIRED";
+
+/** @var kMissingAppCredentialErrorMessage
+ @brief This is the error message the server will respond with if the APNS token is missing in a
+ verifyClient request.
+ */
+static NSString *const kMissingAppCredentialErrorMessage = @"MISSING_APP_CREDENTIAL";
+
+/** @var kMissingAppCredentialErrorMessage
+ @brief This is the error message the server will respond with if the APNS token in a
+ verifyClient request is invalid.
+ */
+static NSString *const kInvalidAppCredentialErrorMessage = @"INVALID_APP_CREDENTIAL";
+
+/** @var kQuoutaExceededErrorMessage
+ @brief This is the error message the server will respond with if the quota for SMS text messages
+ has been exceeded for the project.
+ */
+static NSString *const kQuoutaExceededErrorMessage = @"QUOTA_EXCEEDED";
+
+/** @var kAppNotVerifiedErrorMessage
+ @brief This is the error message the server will respond with if Firebase could not verify the
+ app during a phone authentication flow.
+ */
+static NSString *const kAppNotVerifiedErrorMessage = @"APP_NOT_VERIFIED";
+
+/** @var gBackendImplementation
+ @brief The singleton FIRAuthBackendImplementation instance to use.
+ */
+static id<FIRAuthBackendImplementation> gBackendImplementation;
+
+/** @class FIRAuthBackendRPCImplementation
+ @brief The default RPC-based backend implementation.
+ */
+@interface FIRAuthBackendRPCImplementation : NSObject <FIRAuthBackendImplementation>
+
+/** @property RPCIssuer
+ @brief An instance of FIRAuthBackendRPCIssuer for making RPC requests. Allows the RPC
+ requests/responses to be easily faked.
+ */
+@property(nonatomic, strong) id<FIRAuthBackendRPCIssuer> RPCIssuer;
+
+@end
+
+@implementation FIRAuthBackend
+
++ (id<FIRAuthBackendImplementation>)implementation {
+ if (!gBackendImplementation) {
+ gBackendImplementation = [[FIRAuthBackendRPCImplementation alloc] init];
+ }
+ return gBackendImplementation;
+}
+
++ (void)setBackendImplementation:(id<FIRAuthBackendImplementation>)backendImplementation {
+ gBackendImplementation = backendImplementation;
+}
+
++ (void)setDefaultBackendImplementationWithRPCIssuer:
+ (nullable id<FIRAuthBackendRPCIssuer>)RPCIssuer {
+ FIRAuthBackendRPCImplementation *defaultImplementation =
+ [[FIRAuthBackendRPCImplementation alloc] init];
+ if (RPCIssuer) {
+ defaultImplementation.RPCIssuer = RPCIssuer;
+ }
+ gBackendImplementation = defaultImplementation;
+}
+
++ (void)createAuthURI:(FIRCreateAuthURIRequest *)request
+ callback:(FIRCreateAuthURIResponseCallback)callback {
+ [[self implementation] createAuthURI:request callback:callback];
+}
+
++ (void)getAccountInfo:(FIRGetAccountInfoRequest *)request
+ callback:(FIRGetAccountInfoResponseCallback)callback {
+ [[self implementation] getAccountInfo:request callback:callback];
+}
+
++ (void)setAccountInfo:(FIRSetAccountInfoRequest *)request
+ callback:(FIRSetAccountInfoResponseCallback)callback {
+ [[self implementation] setAccountInfo:request callback:callback];
+}
+
++ (void)verifyAssertion:(FIRVerifyAssertionRequest *)request
+ callback:(FIRVerifyAssertionResponseCallback)callback {
+ [[self implementation] verifyAssertion:request callback:callback];
+}
+
++ (void)verifyCustomToken:(FIRVerifyCustomTokenRequest *)request
+ callback:(FIRVerifyCustomTokenResponseCallback)callback {
+ [[self implementation] verifyCustomToken:request callback:callback];
+}
+
++ (void)verifyPassword:(FIRVerifyPasswordRequest *)request
+ callback:(FIRVerifyPasswordResponseCallback)callback {
+ [[self implementation] verifyPassword:request callback:callback];
+}
+
++ (void)secureToken:(FIRSecureTokenRequest *)request
+ callback:(FIRSecureTokenResponseCallback)callback {
+ [[self implementation] secureToken:request callback:callback];
+}
+
++ (void)getOOBConfirmationCode:(FIRGetOOBConfirmationCodeRequest *)request
+ callback:(FIRGetOOBConfirmationCodeResponseCallback)callback {
+ [[self implementation] getOOBConfirmationCode:request callback:callback];
+}
+
++ (void)signUpNewUser:(FIRSignUpNewUserRequest *)request
+ callback:(FIRSignupNewUserCallback)callback {
+ [[self implementation] signUpNewUser:request callback:callback];
+}
+
++ (void)deleteAccount:(FIRDeleteAccountRequest *)request callback:(FIRDeleteCallBack)callback {
+ [[self implementation] deleteAccount:request callback:callback];
+}
+
++ (void)sendVerificationCode:(FIRSendVerificationCodeRequest *)request
+ callback:(FIRSendVerificationCodeResponseCallback)callback {
+ [[self implementation] sendVerificationCode:request callback:callback];
+}
+
++ (void)verifyPhoneNumber:(FIRVerifyPhoneNumberRequest *)request
+ callback:(FIRVerifyPhoneNumberResponseCallback)callback {
+ [[self implementation] verifyPhoneNumber:request callback:callback];
+}
+
++ (void)verifyClient:(id)request callback:(FIRVerifyClientResponseCallback)callback {
+ [[self implementation] verifyClient:request callback:callback];
+}
+
++ (void)resetPassword:(FIRResetPasswordRequest *)request
+ callback:(FIRResetPasswordCallback)callback {
+ [[self implementation] resetPassword:request callback:callback];
+}
+
+@end
+
+@interface FIRAuthBackendRPCIssuerImplementation : NSObject <FIRAuthBackendRPCIssuer>
+@end
+
+@implementation FIRAuthBackendRPCIssuerImplementation {
+ /** @var The session fetcher service.
+ */
+ GTMSessionFetcherService *_fetcherService;
+}
+
+- (instancetype)init {
+ self = [super init];
+ if (self) {
+ _fetcherService = [[GTMSessionFetcherService alloc] init];
+ _fetcherService.userAgent = [NSString stringWithFormat:@"FirebaseAuth.iOS/%s %@",
+ FirebaseAuthVersionString, GTMFetcherStandardUserAgentString(nil)];
+ _fetcherService.callbackQueue = FIRAuthGlobalWorkQueue();
+ }
+ return self;
+}
+
+- (void)asyncPostToURL:(NSURL *)URL
+ body:(NSData *)body
+ contentType:(NSString *)contentType
+ completionHandler:(void (^)(NSData *_Nullable, NSError *_Nullable))handler {
+ NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:URL];
+ [request setValue:contentType forHTTPHeaderField:@"Content-Type"];
+ NSString *bundleID = [[NSBundle mainBundle] bundleIdentifier];
+ [request setValue:bundleID forHTTPHeaderField:kIosBundleIdentifierHeader];
+
+ NSArray<NSString *> *preferredLocalizations = [NSBundle mainBundle].preferredLocalizations;
+ if (preferredLocalizations.count) {
+ NSString *acceptLanguage = preferredLocalizations.firstObject;
+ [request setValue:acceptLanguage forHTTPHeaderField:@"Accept-Language"];
+ }
+
+ GTMSessionFetcher* fetcher = [_fetcherService fetcherWithRequest:request];
+ fetcher.bodyData = body;
+ [fetcher beginFetchWithCompletionHandler:handler];
+}
+
+@end
+
+@implementation FIRAuthBackendRPCImplementation
+
+- (instancetype)init {
+ self = [super init];
+ if (self) {
+ _RPCIssuer = [[FIRAuthBackendRPCIssuerImplementation alloc] init];
+ }
+ return self;
+}
+
+- (void)createAuthURI:(FIRCreateAuthURIRequest *)request
+ callback:(FIRCreateAuthURIResponseCallback)callback {
+ FIRCreateAuthURIResponse *response = [[FIRCreateAuthURIResponse alloc] init];
+ [self postWithRequest:request response:response callback:^(NSError *error) {
+ if (error) {
+ callback(nil, error);
+ } else {
+ callback(response, nil);
+ }
+ }];
+}
+
+- (void)getAccountInfo:(FIRGetAccountInfoRequest *)request
+ callback:(FIRGetAccountInfoResponseCallback)callback {
+ FIRGetAccountInfoResponse *response = [[FIRGetAccountInfoResponse alloc] init];
+ [self postWithRequest:request response:response callback:^(NSError *error) {
+ if (error) {
+ callback(nil, error);
+ } else {
+ callback(response, nil);
+ }
+ }];
+}
+
+- (void)setAccountInfo:(FIRSetAccountInfoRequest *)request
+ callback:(FIRSetAccountInfoResponseCallback)callback {
+ FIRSetAccountInfoResponse *response = [[FIRSetAccountInfoResponse alloc] init];
+ [self postWithRequest:request response:response callback:^(NSError *error) {
+ if (error) {
+ callback(nil, error);
+ } else {
+ callback(response, nil);
+ }
+ }];
+}
+
+- (void)verifyAssertion:(FIRVerifyAssertionRequest *)request
+ callback:(FIRVerifyAssertionResponseCallback)callback {
+ FIRVerifyAssertionResponse *response = [[FIRVerifyAssertionResponse alloc] init];
+ [self postWithRequest:request response:response callback:^(NSError *error) {
+ if (error) {
+ callback(nil, error);
+ return;
+ }
+ callback(response, nil);
+ }];
+}
+
+- (void)verifyCustomToken:(FIRVerifyCustomTokenRequest *)request
+ callback:(FIRVerifyCustomTokenResponseCallback)callback {
+ FIRVerifyCustomTokenResponse *response = [[FIRVerifyCustomTokenResponse alloc] init];
+ [self postWithRequest:request response:response callback:^(NSError *error) {
+ if (error) {
+ callback(nil, error);
+ } else {
+ callback(response, nil);
+ }
+ }];
+}
+
+- (void)verifyPassword:(FIRVerifyPasswordRequest *)request
+ callback:(FIRVerifyPasswordResponseCallback)callback {
+ FIRVerifyPasswordResponse *response = [[FIRVerifyPasswordResponse alloc] init];
+ [self postWithRequest:request response:response callback:^(NSError *error) {
+ if (error) {
+ callback(nil, error);
+ } else {
+ callback(response, nil);
+ }
+ }];
+}
+
+- (void)secureToken:(FIRSecureTokenRequest *)request
+ callback:(FIRSecureTokenResponseCallback)callback {
+ FIRSecureTokenResponse *response = [[FIRSecureTokenResponse alloc] init];
+ [self postWithRequest:request response:response callback:^(NSError *error) {
+ if (error) {
+ callback(nil, error);
+ } else {
+ callback(response, nil);
+ }
+ }];
+}
+
+- (void)getOOBConfirmationCode:(FIRGetOOBConfirmationCodeRequest *)request
+ callback:(FIRGetOOBConfirmationCodeResponseCallback)callback {
+ FIRGetOOBConfirmationCodeResponse *response = [[FIRGetOOBConfirmationCodeResponse alloc] init];
+ [self postWithRequest:request response:response callback:^(NSError *error) {
+ if (error) {
+ callback(nil, error);
+ } else {
+ callback(response, nil);
+ }
+ }];
+}
+
+- (void)signUpNewUser:(FIRSignUpNewUserRequest *)request
+ callback:(FIRSignupNewUserCallback)callback{
+ FIRSignUpNewUserResponse *response = [[FIRSignUpNewUserResponse alloc] init];
+ [self postWithRequest:request response:response callback:^(NSError *error) {
+ if (error) {
+ callback(nil, error);
+ } else {
+ callback(response, nil);
+ }
+ }];
+}
+
+- (void)deleteAccount:(FIRDeleteAccountRequest *)request callback:(FIRDeleteCallBack)callback {
+ FIRDeleteAccountResponse *response = [[FIRDeleteAccountResponse alloc] init];
+ [self postWithRequest:request response:response callback:callback];
+}
+
+- (void)sendVerificationCode:(FIRSendVerificationCodeRequest *)request
+ callback:(FIRSendVerificationCodeResponseCallback)callback {
+ FIRSendVerificationCodeResponse *response = [[FIRSendVerificationCodeResponse alloc] init];
+ [self postWithRequest:request response:response callback:^(NSError *error) {
+ if (error) {
+ callback(nil, error);
+ } else {
+ callback(response, error);
+ }
+ }];
+}
+
+- (void)verifyPhoneNumber:(FIRVerifyPhoneNumberRequest *)request
+ callback:(FIRVerifyPhoneNumberResponseCallback)callback {
+ FIRVerifyPhoneNumberResponse *response = [[FIRVerifyPhoneNumberResponse alloc] init];
+ [self postWithRequest:request response:response callback:^(NSError *error) {
+ if (error) {
+ callback(nil, error);
+ return;
+ }
+ // Check whether or not the successful response is actually the special case phone auth flow
+ // that returns a temporary proof and phone number.
+ if (response.phoneNumber.length && response.temporaryProof.length) {
+ FIRPhoneAuthCredential *credential =
+ [[FIRPhoneAuthCredential alloc] initWithTemporaryProof:response.temporaryProof
+ phoneNumber:response.phoneNumber
+ providerID:FIRPhoneAuthProviderID];
+ callback(nil,
+ [FIRAuthErrorUtils credentialAlreadyInUseErrorWithMessage:nil
+ credential:credential]);
+ return;
+ }
+ callback(response, nil);
+ }];
+}
+
+- (void)verifyClient:(id)request callback:(FIRVerifyClientResponseCallback)callback {
+ FIRVerifyClientResponse *response = [[FIRVerifyClientResponse alloc] init];
+ [self postWithRequest:request response:response callback:^(NSError *error) {
+ if (error) {
+ callback(nil, error);
+ return;
+ }
+ callback(response, nil);
+ }];
+}
+
+- (void)resetPassword:(FIRResetPasswordRequest *)request
+ callback:(FIRResetPasswordCallback)callback {
+ FIRResetPasswordResponse *response = [[FIRResetPasswordResponse alloc] init];
+ [self postWithRequest:request response:response callback:^(NSError *error) {
+ if (error) {
+ callback(nil, error);
+ return;
+ }
+ callback(response, nil);
+ }];
+}
+
+#pragma mark - Generic RPC handling methods
+
+/** @fn postWithRequest:response:callback:
+ @brief Calls the RPC using HTTP POST.
+ @remarks Possible error responses:
+ @see FIRAuthInternalErrorCodeRPCRequestEncodingError
+ @see FIRAuthInternalErrorCodeJSONSerializationError
+ @see FIRAuthInternalErrorCodeNetworkError
+ @see FIRAuthInternalErrorCodeUnexpectedErrorResponse
+ @see FIRAuthInternalErrorCodeUnexpectedResponse
+ @see FIRAuthInternalErrorCodeRPCResponseDecodingError
+ @param request The request.
+ @param response The empty response to be filled.
+ @param callback The callback for both success and failure.
+ */
+- (void)postWithRequest:(id<FIRAuthRPCRequest>)request
+ response:(id<FIRAuthRPCResponse>)response
+ callback:(void (^)(NSError *error))callback {
+ NSError *error;
+ id postBody = [request unencodedHTTPRequestBodyWithError:&error];
+ if (!postBody) {
+ callback([FIRAuthErrorUtils RPCRequestEncodingErrorWithUnderlyingError:error]);
+ return;
+ }
+ NSJSONWritingOptions JSONWritingOptions = 0;
+ #if DEBUG
+ JSONWritingOptions |= NSJSONWritingPrettyPrinted;
+ #endif
+
+ NSData *bodyData;
+ if ([NSJSONSerialization isValidJSONObject:postBody]) {
+ bodyData = [NSJSONSerialization dataWithJSONObject:postBody
+ options:JSONWritingOptions
+ error:&error];
+ if (!bodyData) {
+ // This is an untested case. This happens exclusively when there is an error in the framework
+ // implementation of dataWithJSONObject:options:error:. This shouldn't normally occur as
+ // isValidJSONObject: should return NO in any case we should encounter an error.
+ error = [FIRAuthErrorUtils JSONSerializationErrorWithUnderlyingError:error];
+ }
+ } else {
+ error = [FIRAuthErrorUtils JSONSerializationErrorForUnencodableType];
+ }
+ if (!bodyData) {
+ callback(error);
+ return;
+ }
+
+ [_RPCIssuer asyncPostToURL:[request requestURL]
+ body:bodyData
+ contentType:kJSONContentType
+ completionHandler:^(NSData *data, NSError *error) {
+ // If there is an error with no body data at all, then this must be a network error.
+ if (error && !data) {
+ callback([FIRAuthErrorUtils networkErrorWithUnderlyingError:error]);
+ return;
+ }
+
+ // Try to decode the HTTP response data which may contain either a successful response or error
+ // message.
+ NSError *jsonError;
+ NSDictionary * dictionary =
+ [NSJSONSerialization JSONObjectWithData:data
+ options:NSJSONReadingMutableLeaves
+ error:&jsonError];
+ if (!dictionary) {
+ if (error) {
+ // We have an error, but we couldn't decode the body, so we have no additional information
+ // other than the raw response and the original NSError (the jsonError is infered by the
+ // error code (FIRAuthErrorCodeUnexpectedHTTPResponse, and is irrelevant.)
+ callback([FIRAuthErrorUtils unexpectedErrorResponseWithData:data underlyingError:error]);
+ } else {
+ // This is supposed to be a "successful" response, but we couldn't deserialize the body.
+ callback([FIRAuthErrorUtils unexpectedResponseWithData:data underlyingError:jsonError]);
+ }
+ return;
+ }
+ if (![dictionary isKindOfClass:[NSDictionary class]]) {
+ if (error) {
+ callback([FIRAuthErrorUtils unexpectedErrorResponseWithDeserializedResponse:dictionary]);
+ } else {
+ callback([FIRAuthErrorUtils unexpectedResponseWithDeserializedResponse:dictionary]);
+ }
+ return;
+ }
+
+ // At this point we either have an error with successfully decoded details in the body, or we
+ // have a response which must pass further validation before we know it's truly successful.
+ // We deal with the case where we have an error with successfully decoded error details first:
+ if (error) {
+ NSDictionary *errorDictionary = dictionary[kErrorKey];
+ if ([errorDictionary isKindOfClass:[NSDictionary class]]) {
+ id<NSObject> errorMessage = errorDictionary[kErrorMessageKey];
+ if ([errorMessage isKindOfClass:[NSString class]]) {
+ NSString *errorMessageString = (NSString *)errorMessage;
+
+ // Contruct client error.
+ NSError *clientError = [[self class] clientErrorWithServerErrorMessage:errorMessageString
+ errorDictionary:errorDictionary
+ response:response];
+ if (clientError) {
+ callback(clientError);
+ return;
+ }
+ }
+ // Not a message we know, return the message directly.
+ if (errorMessage) {
+ NSError *unexpecterErrorResponse =
+ [FIRAuthErrorUtils unexpectedErrorResponseWithDeserializedResponse:errorDictionary];
+ callback(unexpecterErrorResponse);
+ return;
+ }
+ }
+ // No error message at all, return the decoded response.
+ callback([FIRAuthErrorUtils unexpectedErrorResponseWithDeserializedResponse:dictionary]);
+ return;
+ }
+
+ // Finally, we try to populate the response object with the JSON values.
+ if (![response setWithDictionary:dictionary error:&error]) {
+ callback([FIRAuthErrorUtils RPCResponseDecodingErrorWithDeserializedResponse:dictionary
+ underlyingError:error]);
+ return;
+ }
+
+ // Success! The response object originally passed in can be used by the caller.
+ callback(nil);
+ }];
+}
+
+/** @fn clientErrorWithServerErrorMessage:errorDictionary:
+ @brief Translates known server errors to client errors.
+ @param serverErrorMessage The error message from the server.
+ @param errorDictionary The error part of the response from the server.
+ @param response The response from the server RPC.
+ @return A client error, if any.
+ */
++ (nullable NSError *)clientErrorWithServerErrorMessage:(NSString *)serverErrorMessage
+ errorDictionary:(NSDictionary *)errorDictionary
+ response:(id<FIRAuthRPCResponse>)response {
+ NSString *shortErrorMessage = serverErrorMessage;
+ NSString *serverDetailErrorMessage;
+ NSRange colonRange = [serverErrorMessage rangeOfString:@":"];
+ if (colonRange.location != NSNotFound) {
+ shortErrorMessage = [serverErrorMessage substringToIndex:colonRange.location];
+ shortErrorMessage =
+ [shortErrorMessage stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
+ serverDetailErrorMessage = [serverErrorMessage substringFromIndex:colonRange.location + 1];
+ serverDetailErrorMessage = [serverDetailErrorMessage stringByTrimmingCharactersInSet:
+ [NSCharacterSet whitespaceCharacterSet]];
+ }
+
+ // Delegate the responsibility for constructing the client error to the response object,
+ // if possible.
+ SEL clientErrorWithServerErrorMessageSelector =
+ @selector(clientErrorWithShortErrorMessage:detailErrorMessage:);
+ if ([response respondsToSelector:clientErrorWithServerErrorMessageSelector]) {
+ NSError *error = [response clientErrorWithShortErrorMessage:shortErrorMessage
+ detailErrorMessage:serverDetailErrorMessage];
+ if (error) {
+ return error;
+ }
+ }
+
+ if ([shortErrorMessage isEqualToString:kUserNotFoundErrorMessage]) {
+ return [FIRAuthErrorUtils userNotFoundErrorWithMessage:serverDetailErrorMessage];
+ }
+
+ if ([shortErrorMessage isEqualToString:kUserDeletedErrorMessage]) {
+ return [FIRAuthErrorUtils userNotFoundErrorWithMessage:serverDetailErrorMessage];
+ }
+
+ if ([shortErrorMessage isEqualToString:kInvalidLocalIDErrorMessage]) {
+ // This case shouldn't be necessary but it is for now: b/27908364 .
+ return [FIRAuthErrorUtils userNotFoundErrorWithMessage:serverDetailErrorMessage];
+ }
+
+ if ([shortErrorMessage isEqualToString:kUserTokenExpiredErrorMessage]) {
+ return [FIRAuthErrorUtils userTokenExpiredErrorWithMessage:serverDetailErrorMessage];
+ }
+
+ if ([shortErrorMessage isEqualToString:kTooManyRequestsErrorMessage]) {
+ return [FIRAuthErrorUtils tooManyRequestsErrorWithMessage:serverDetailErrorMessage];
+ }
+
+ if ([shortErrorMessage isEqualToString:kInvalidCustomTokenErrorMessage]) {
+ return [FIRAuthErrorUtils invalidCustomTokenErrorWithMessage:serverDetailErrorMessage];
+ }
+
+ if ([shortErrorMessage isEqualToString:kCustomTokenMismatch]) {
+ return [FIRAuthErrorUtils customTokenMistmatchErrorWithMessage:serverDetailErrorMessage];
+ }
+
+ if ([shortErrorMessage isEqualToString:kInvalidCredentialErrorMessage]) {
+ return [FIRAuthErrorUtils invalidCredentialErrorWithMessage:serverDetailErrorMessage];
+ }
+
+ if ([shortErrorMessage isEqualToString:kUserDisabledErrorMessage]) {
+ return [FIRAuthErrorUtils userDisabledErrorWithMessage:serverDetailErrorMessage];
+ }
+
+ if ([shortErrorMessage isEqualToString:kOperationNotAllowedErrorMessage]) {
+ return [FIRAuthErrorUtils operationNotAllowedErrorWithMessage:serverDetailErrorMessage];
+ }
+
+ if ([shortErrorMessage isEqualToString:kPasswordLoginDisabledErrorMessage]) {
+ return [FIRAuthErrorUtils operationNotAllowedErrorWithMessage:serverDetailErrorMessage];
+ }
+
+ if ([shortErrorMessage isEqualToString:kEmailAlreadyInUseErrorMessage]) {
+ return [FIRAuthErrorUtils emailAlreadyInUseErrorWithEmail:nil];
+ }
+
+ if ([shortErrorMessage isEqualToString:kInvalidEmailErrorMessage]) {
+ return [FIRAuthErrorUtils invalidEmailErrorWithMessage:serverDetailErrorMessage];
+ }
+
+ // "INVALID_IDENTIFIER" can be returned by createAuthURI RPC. Considering email addresses are
+ // currently the only identifiers, we surface the FIRAuthErrorCodeInvalidEmail error code in this
+ // case.
+ if ([shortErrorMessage isEqualToString:kInvalidIdentifierErrorMessage]) {
+ return [FIRAuthErrorUtils invalidEmailErrorWithMessage:serverDetailErrorMessage];
+ }
+
+ if ([shortErrorMessage isEqualToString:kWrongPasswordErrorMessage]) {
+ return [FIRAuthErrorUtils wrongPasswordErrorWithMessage:serverDetailErrorMessage];
+ }
+
+ if ([shortErrorMessage isEqualToString:kCredentialTooOldErrorMessage]) {
+ return [FIRAuthErrorUtils requiresRecentLoginErrorWithMessage:serverDetailErrorMessage];
+ }
+
+ if ([shortErrorMessage isEqualToString:kInvalidUserTokenErrorMessage]) {
+ return [FIRAuthErrorUtils invalidUserTokenErrorWithMessage:serverDetailErrorMessage];
+ }
+
+ if ([shortErrorMessage isEqualToString:kFederatedUserIDAlreadyLinkedMessage]) {
+ return [FIRAuthErrorUtils credentialAlreadyInUseErrorWithMessage:serverDetailErrorMessage
+ credential:nil];
+ }
+
+ if ([shortErrorMessage isEqualToString:kWeakPasswordErrorMessagePrefix]) {
+ return [FIRAuthErrorUtils weakPasswordErrorWithServerResponseReason:serverDetailErrorMessage];
+ }
+
+ if ([shortErrorMessage isEqualToString:kExpiredActionCodeErrorMessage]) {
+ return [FIRAuthErrorUtils expiredActionCodeErrorWithMessage:serverDetailErrorMessage];
+ }
+
+ if ([shortErrorMessage isEqualToString:kInvalidActionCodeErrorMessage]) {
+ return [FIRAuthErrorUtils invalidActionCodeErrorWithMessage:serverDetailErrorMessage];
+ }
+
+ if ([shortErrorMessage isEqualToString:kInvalidSenderEmailErrorMessage]) {
+ return [FIRAuthErrorUtils invalidSenderErrorWithMessage:serverDetailErrorMessage];
+ }
+
+ if ([shortErrorMessage isEqualToString:kInvalidMessagePayloadErrorMessage]) {
+ return [FIRAuthErrorUtils invalidMessagePayloadErrorWithMessage:serverDetailErrorMessage];
+ }
+
+ if ([shortErrorMessage isEqualToString:kInvalidRecipientEmailErrorMessage]) {
+ return [FIRAuthErrorUtils invalidRecipientEmailErrorWithMessage:serverDetailErrorMessage];
+ }
+
+ if ([shortErrorMessage isEqualToString:kInvalidPhoneNumberErrorMessage]) {
+ return [FIRAuthErrorUtils invalidPhoneNumberErrorWithMessage:serverDetailErrorMessage];
+ }
+
+ if ([shortErrorMessage isEqualToString:kInvalidSessionInfoErrorMessage]) {
+ return [FIRAuthErrorUtils invalidVerificationIDErrorWithMessage:serverDetailErrorMessage];
+ }
+
+ if ([shortErrorMessage isEqualToString:kInvalidVerificationCodeErrorMessage]) {
+ return [FIRAuthErrorUtils invalidVerificationCodeErrorWithMessage:serverDetailErrorMessage];
+ }
+
+ if ([shortErrorMessage isEqualToString:kSessionExpiredErrorMessage]) {
+ return [FIRAuthErrorUtils sessionExpiredErrorWithMessage:serverDetailErrorMessage];
+ }
+
+ if ([shortErrorMessage isEqualToString:kMissingAppCredentialErrorMessage]) {
+ return [FIRAuthErrorUtils missingAppCredentialWithMessage:serverDetailErrorMessage];
+ }
+
+ if ([shortErrorMessage isEqualToString:kInvalidAppCredentialErrorMessage]) {
+ return [FIRAuthErrorUtils invalidAppCredentialWithMessage:serverDetailErrorMessage];
+ }
+
+ if ([shortErrorMessage isEqualToString:kQuoutaExceededErrorMessage]) {
+ return [FIRAuthErrorUtils quotaExceededErrorWithMessage:serverErrorMessage];
+ }
+
+ if ([shortErrorMessage isEqualToString:kAppNotVerifiedErrorMessage]) {
+ return [FIRAuthErrorUtils appNotVerifiedErrorWithMessage:serverErrorMessage];
+ }
+
+ // In this case we handle an error that might be specified in the underlying errors dictionary,
+ // the error message in determined based on the @c reason key in the dictionary.
+ if (errorDictionary[kErrorsKey]) {
+ // Check for underlying error with reason = keyInvalid;
+ id underlyingErrors = errorDictionary[kErrorsKey];
+ if ([underlyingErrors isKindOfClass:[NSArray class]]) {
+ NSArray *underlyingErrorsArray = (NSArray *)underlyingErrors;
+ for (id underlyingError in underlyingErrorsArray) {
+ if ([underlyingError isKindOfClass:[NSDictionary class]]) {
+ NSDictionary *underlyingErrorDictionary = (NSDictionary *)underlyingError;
+ NSString *reason = underlyingErrorDictionary[kReasonKey];
+ if ([reason hasPrefix:kInvalidKeyReasonValue]) {
+ return [FIRAuthErrorUtils invalidAPIKeyError];
+ }
+ if ([reason isEqualToString:kAppNotAuthorizedReasonValue]) {
+ return [FIRAuthErrorUtils appNotAuthorizedError];
+ }
+ }
+ }
+ }
+ }
+ return nil;
+}
+
+@end
diff --git a/Firebase/Auth/Source/RPCs/FIRAuthRPCRequest.h b/Firebase/Auth/Source/RPCs/FIRAuthRPCRequest.h
new file mode 100644
index 0000000..ddad3cb
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRAuthRPCRequest.h
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @protocol FIRAuthRPCRequest
+ @brief The generic interface for an RPC request needed by @c FIRAuthBackend.
+ */
+@protocol FIRAuthRPCRequest <NSObject>
+
+/** @fn requestURL
+ @brief Gets the request's full URL.
+ */
+- (NSURL *)requestURL;
+
+/** @fn UnencodedHTTPRequestBodyWithError:
+ @brief Creates unencoded HTTP body representing the request.
+ @param error An out field for an error which occurred constructing the request.
+ @return The HTTP body data representing the request before any encoding, or nil for error.
+ */
+- (nullable id)unencodedHTTPRequestBodyWithError:(NSError *_Nullable *_Nullable)error;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/RPCs/FIRAuthRPCResponse.h b/Firebase/Auth/Source/RPCs/FIRAuthRPCResponse.h
new file mode 100644
index 0000000..c130f3f
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRAuthRPCResponse.h
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @protocol FIRAuthRPCResponse
+ @brief The generic interface for an RPC response needed by @c FIRAuthBackend.
+ */
+@protocol FIRAuthRPCResponse <NSObject>
+
+/** @fn setFieldsWithDictionary:error:
+ @brief Sets the response instance from the decoded JSON response.
+ @param dictionary The dictionary decoded from HTTP JSON response.
+ @param error An out field for an error which occurred constructing the request.
+ @return Whether the operation was successful or not.
+ */
+- (BOOL)setWithDictionary:(NSDictionary *)dictionary
+ error:(NSError *_Nullable *_Nullable)error;
+
+@optional
+
+/** @fn clientErrorWithshortErrorMessage:detailErrorMessage
+ @brief This optional method allows response classes to create client errors given a short error
+ message and a detail error message from the server.
+ @param shortErrorMessage The short error message from the server.
+ @param detailErrorMessage The detailed error message from the server.
+ @return A client error, if any.
+ */
+- (nullable NSError *)clientErrorWithShortErrorMessage:(NSString *)shortErrorMessage
+ detailErrorMessage:(NSString *)detailErrorMessage;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/RPCs/FIRCreateAuthURIRequest.h b/Firebase/Auth/Source/RPCs/FIRCreateAuthURIRequest.h
new file mode 100644
index 0000000..bb28826
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRCreateAuthURIRequest.h
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+#import "FIRAuthRPCRequest.h"
+#import "FIRIdentityToolkitRequest.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @class FIRCreateAuthURIRequest
+ @brief Represents the parameters for the createAuthUri endpoint.
+ @see https://developers.google.com/identity/toolkit/web/reference/relyingparty/createAuthUri
+ */
+@interface FIRCreateAuthURIRequest : FIRIdentityToolkitRequest <FIRAuthRPCRequest>
+
+/** @property identifier
+ @brief The email or federated ID of the user.
+ */
+@property(nonatomic, copy) NSString *identifier;
+
+/** @property continueURI
+ @brief The URI to which the IDP redirects the user after the federated login flow.
+ */
+@property(nonatomic, copy) NSString *continueURI;
+
+/** @property openIDRealm
+ @brief Optional realm for OpenID protocol. The sub string "scheme://domain:port" of the param
+ "continueUri" is used if this is not set.
+ */
+@property(nonatomic, copy, nullable) NSString *openIDRealm;
+
+/** @property providerID
+ @brief The IdP ID. For white listed IdPs it's a short domain name e.g. google.com, aol.com,
+ live.net and yahoo.com. For other OpenID IdPs it's the OP identifier.
+ */
+@property(nonatomic, copy, nullable) NSString *providerID;
+
+/** @property clientID
+ @brief The relying party OAuth client ID.
+ */
+@property(nonatomic, copy, nullable) NSString *clientID;
+
+/** @property context
+ @brief The opaque value used by the client to maintain context info between the authentication
+ request and the IDP callback.
+ */
+@property(nonatomic, copy, nullable) NSString *context;
+
+/** @property appID
+ @brief The iOS client application's bundle identifier.
+ */
+@property(nonatomic, copy, nullable) NSString *appID;
+
+/** @fn initWithEndpoint:APIKey:
+ @brief Please use initWithIdentifier:continueURI:APIKey:
+ */
+- (nullable instancetype)initWithEndpoint:(NSString *)endpoint
+ APIKey:(NSString *)APIKey NS_UNAVAILABLE;
+
+/** @fn initWithIdentifier:continueURI:APIKey:
+ @brief Designated initializer.
+ @param identifier The email or federated ID of the user.
+ @param continueURI The URI to which the IDP redirects the user after the federated login flow.
+ @param APIKey The client's API Key.
+ */
+- (nullable instancetype)initWithIdentifier:(NSString *)identifier
+ continueURI:(NSString *)continueURI
+ APIKey:(NSString *)APIKey NS_DESIGNATED_INITIALIZER;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/RPCs/FIRCreateAuthURIRequest.m b/Firebase/Auth/Source/RPCs/FIRCreateAuthURIRequest.m
new file mode 100644
index 0000000..6d2b9e9
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRCreateAuthURIRequest.m
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRCreateAuthURIRequest.h"
+
+/** @var kCreateAuthURIEndpoint
+ @brief The "createAuthUri" endpoint.
+ */
+static NSString *const kCreateAuthURIEndpoint = @"createAuthUri";
+
+/** @var kProviderIDKey
+ @brief The key for the "providerId" value in the request.
+ */
+static NSString *const kProviderIDKey = @"providerId";
+
+/** @var kIdentifierKey
+ @brief The key for the "identifier" value in the request.
+ */
+static NSString *const kIdentifierKey = @"identifier";
+
+/** @var kContinueURIKey
+ @brief The key for the "continueUri" value in the request.
+ */
+static NSString *const kContinueURIKey = @"continueUri";
+
+/** @var kOpenIDRealmKey
+ @brief The key for the "openidRealm" value in the request.
+ */
+static NSString *const kOpenIDRealmKey = @"openidRealm";
+
+/** @var kClientIDKey
+ @brief The key for the "clientId" value in the request.
+ */
+static NSString *const kClientIDKey = @"clientId";
+
+/** @var kContextKey
+ @brief The key for the "context" value in the request.
+ */
+static NSString *const kContextKey = @"context";
+
+/** @var kAppIDKey
+ @brief The key for the "appId" value in the request.
+ */
+static NSString *const kAppIDKey = @"appId";
+
+@implementation FIRCreateAuthURIRequest
+
+- (nullable instancetype)initWithIdentifier:(NSString *)identifier
+ continueURI:(NSString *)continueURI
+ APIKey:(NSString *)APIKey {
+ self = [super initWithEndpoint:kCreateAuthURIEndpoint APIKey:APIKey];
+ if (self) {
+ _identifier = [identifier copy];
+ _continueURI = [continueURI copy];
+ }
+ return self;
+}
+
+- (nullable id)unencodedHTTPRequestBodyWithError:(NSError *_Nullable *_Nullable)error {
+ NSMutableDictionary *postBody = [@{
+ kIdentifierKey : _identifier,
+ kContinueURIKey : _continueURI
+ } mutableCopy];
+ if (_providerID) {
+ postBody[kProviderIDKey] = _providerID;
+ }
+ if (_openIDRealm) {
+ postBody[kOpenIDRealmKey] = _openIDRealm;
+ }
+ if (_clientID) {
+ postBody[kClientIDKey] = _clientID;
+ }
+ if (_context) {
+ postBody[kContextKey] = _context;
+ }
+ if (_appID) {
+ postBody[kAppIDKey] = _appID;
+ }
+ return postBody;
+}
+
+@end
diff --git a/Firebase/Auth/Source/RPCs/FIRCreateAuthURIResponse.h b/Firebase/Auth/Source/RPCs/FIRCreateAuthURIResponse.h
new file mode 100644
index 0000000..9f6cbae
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRCreateAuthURIResponse.h
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+#import "FIRAuthRPCResponse.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @class FIRCreateAuthURIResponse
+ @brief Represents the parameters for the createAuthUri endpoint.
+ @see https://developers.google.com/identity/toolkit/web/reference/relyingparty/createAuthUri
+ */
+@interface FIRCreateAuthURIResponse : NSObject <FIRAuthRPCResponse>
+
+/** @property authUri
+ @brief The URI used by the IDP to authenticate the user.
+ */
+@property(nonatomic, strong, readonly, nullable) NSString *authURI;
+
+/** @property registered
+ @brief Whether the user is registered if the identifier is an email.
+ */
+@property(nonatomic, assign, readonly) BOOL registered;
+
+/** @property providerId
+ @brief The provider ID of the auth URI.
+ */
+@property(nonatomic, strong, readonly, nullable) NSString *providerID;
+
+/** @property forExistingProvider
+ @brief True if the authUri is for user's existing provider.
+ */
+@property(nonatomic, assign, readonly) BOOL forExistingProvider;
+
+/** @property allProviders
+ @brief A list of provider IDs the passed @c identifier could use to sign in with.
+ */
+@property(nonatomic, copy, readonly, nullable) NSArray<NSString *> *allProviders;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/RPCs/FIRCreateAuthURIResponse.m b/Firebase/Auth/Source/RPCs/FIRCreateAuthURIResponse.m
new file mode 100644
index 0000000..7a38cca
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRCreateAuthURIResponse.m
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRCreateAuthURIResponse.h"
+
+#import "../Private/FIRAuthErrorUtils.h"
+
+@implementation FIRCreateAuthURIResponse
+
+- (BOOL)setWithDictionary:(NSDictionary *)dictionary
+ error:(NSError *_Nullable *_Nullable)error {
+ _providerID = [dictionary[@"providerId"] copy];
+ _authURI = [dictionary[@"authUri"] copy];
+ _registered = [dictionary[@"registered"] boolValue];
+ _forExistingProvider = [dictionary[@"forExistingProvider"] boolValue];
+ _allProviders = [dictionary[@"allProviders"] copy];
+ return YES;
+}
+
+@end
diff --git a/Firebase/Auth/Source/RPCs/FIRDeleteAccountRequest.h b/Firebase/Auth/Source/RPCs/FIRDeleteAccountRequest.h
new file mode 100644
index 0000000..1751e54
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRDeleteAccountRequest.h
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+#import "FIRAuthRPCRequest.h"
+#import "FIRIdentityToolkitRequest.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @class FIRDeleteAccountRequest
+ @brief Represents the parameters for the deleteAccount endpoint.
+ @see https://developers.google.com/identity/toolkit/web/reference/relyingparty/deleteAccount
+ */
+@interface FIRDeleteAccountRequest : FIRIdentityToolkitRequest<FIRAuthRPCRequest>
+
+/** @fn initWithEndpoint:APIKey:
+ @brief Please use initWithAPIKey:
+ */
+- (nullable instancetype)initWithEndpoint:(NSString *)endpoint
+ APIKey:(NSString *)APIKey NS_UNAVAILABLE;
+
+/** @fn initWithAPIKey:
+ @brief Designated initializer.
+ @param APIKey The client's API Key.
+ @param localID The local ID.
+ @param accessToken The access token.
+ */
+- (nullable instancetype)initWithAPIKey:(NSString *)APIKey
+ localID:(NSString *)localID
+ accessToken:(NSString *)accessToken NS_DESIGNATED_INITIALIZER;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/RPCs/FIRDeleteAccountRequest.m b/Firebase/Auth/Source/RPCs/FIRDeleteAccountRequest.m
new file mode 100644
index 0000000..9105ba0
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRDeleteAccountRequest.m
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRDeleteAccountRequest.h"
+
+/** @var kCreateAuthURIEndpoint
+ @brief The "deleteAccount" endpoint.
+ */
+static NSString *const kDeleteAccountEndpoint = @"deleteAccount";
+
+/** @var kIDTokenKey
+ @brief The key for the "idToken" value in the request. This is actually the STS Access Token,
+ despite it's confusing (backwards compatiable) parameter name.
+ */
+static NSString *const kIDTokenKey = @"idToken";
+
+/** @var kLocalIDKey
+ @brief The key for the "localID" value in the request.
+ */
+static NSString *const kLocalIDKey = @"localId";
+
+@implementation FIRDeleteAccountRequest {
+ /** @var _accessToken
+ @brief The STS Access Token of the authenticated user.
+ */
+ NSString *_accessToken;
+
+ /** @var _localID
+ @brief The localID of the user.
+ */
+ NSString *_localID;
+}
+
+- (nullable instancetype)initWithAPIKey:(NSString *)APIKey
+ localID:(nonnull NSString *)localID
+ accessToken:(nonnull NSString *)accessToken {
+ self = [super initWithEndpoint:kDeleteAccountEndpoint APIKey:APIKey];
+ if (self) {
+ _localID = [localID copy];
+ _accessToken = [accessToken copy];
+ }
+ return self;
+}
+
+- (nullable id)unencodedHTTPRequestBodyWithError:(NSError *_Nullable *_Nullable)error {
+ NSMutableDictionary *postBody = [NSMutableDictionary dictionary];
+ postBody[kIDTokenKey] = _accessToken;
+ postBody[kLocalIDKey] = _localID;
+ return postBody;
+}
+
+@end
diff --git a/Firebase/Auth/Source/RPCs/FIRDeleteAccountResponse.h b/Firebase/Auth/Source/RPCs/FIRDeleteAccountResponse.h
new file mode 100644
index 0000000..59226d6
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRDeleteAccountResponse.h
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+#import "FIRAuthRPCResponse.h"
+
+/** @class FIRDeleteAccountResponse
+ @brief Represents the response from the deleteAccount endpoint.
+ @see https://developers.google.com/identity/toolkit/web/reference/relyingparty/deleteAccount
+ */
+@interface FIRDeleteAccountResponse : NSObject<FIRAuthRPCResponse>
+@end
diff --git a/Firebase/Auth/Source/RPCs/FIRDeleteAccountResponse.m b/Firebase/Auth/Source/RPCs/FIRDeleteAccountResponse.m
new file mode 100644
index 0000000..01eb0a5
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRDeleteAccountResponse.m
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRDeleteAccountResponse.h"
+
+#import "../Private/FIRAuthErrorUtils.h"
+
+@implementation FIRDeleteAccountResponse
+
+- (BOOL)setWithDictionary:(NSDictionary *)dictionary
+ error:(NSError *_Nullable *_Nullable)error {
+ return YES;
+}
+
+@end
diff --git a/Firebase/Auth/Source/RPCs/FIRGetAccountInfoRequest.h b/Firebase/Auth/Source/RPCs/FIRGetAccountInfoRequest.h
new file mode 100644
index 0000000..b45b933
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRGetAccountInfoRequest.h
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+#import "FIRAuthRPCRequest.h"
+#import "FIRIdentityToolkitRequest.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @class FIRGetAccountInfoRequest
+ @brief Represents the parameters for the getAccountInfo endpoint.
+ @see https://developers.google.com/identity/toolkit/web/reference/relyingparty/getAccountInfo
+ */
+@interface FIRGetAccountInfoRequest : FIRIdentityToolkitRequest <FIRAuthRPCRequest>
+
+/** @property accessToken
+ @brief The STS Access Token for the authenticated user.
+ */
+@property(nonatomic, copy) NSString *accessToken;
+
+/** @fn initWithEndpoint:APIKey:
+ @brief Please use initWithAPIKey:IDToken:
+ */
+- (nullable instancetype)initWithEndpoint:(NSString *)endpoint
+ APIKey:(NSString *)APIKey NS_UNAVAILABLE;
+
+/** @fn initWithAPIKey:accessToken:
+ @brief Designated initializer.
+ @param APIKey The client's API Key.
+ @param accessToken The Access Token of the authenticated user.
+ */
+- (nullable instancetype)initWithAPIKey:(NSString *)APIKey
+ accessToken:(NSString *)accessToken NS_DESIGNATED_INITIALIZER;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/RPCs/FIRGetAccountInfoRequest.m b/Firebase/Auth/Source/RPCs/FIRGetAccountInfoRequest.m
new file mode 100644
index 0000000..5c73086
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRGetAccountInfoRequest.m
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRGetAccountInfoRequest.h"
+
+/** @var kGetAccountInfoEndpoint
+ @brief The "getAccountInfo" endpoint.
+ */
+static NSString *const kGetAccountInfoEndpoint = @"getAccountInfo";
+
+/** @var kIDTokenKey
+ @brief The key for the "idToken" value in the request. This is actually the STS Access Token,
+ despite it's confusing (backwards compatiable) parameter name.
+ */
+static NSString *const kIDTokenKey = @"idToken";
+
+@implementation FIRGetAccountInfoRequest
+
+- (nullable instancetype)initWithAPIKey:(NSString *)APIKey
+ accessToken:(NSString *)accessToken {
+ self = [super initWithEndpoint:kGetAccountInfoEndpoint APIKey:APIKey];
+ if (self) {
+ _accessToken = [accessToken copy];
+ }
+ return self;
+}
+
+- (nullable id)unencodedHTTPRequestBodyWithError:(NSError *_Nullable *_Nullable)error {
+ return @{
+ kIDTokenKey : _accessToken
+ };
+}
+
+@end
diff --git a/Firebase/Auth/Source/RPCs/FIRGetAccountInfoResponse.h b/Firebase/Auth/Source/RPCs/FIRGetAccountInfoResponse.h
new file mode 100644
index 0000000..009f3c1
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRGetAccountInfoResponse.h
@@ -0,0 +1,146 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+#import "FIRAuthRPCResponse.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @class FIRGetAccountInfoResponseProviderUserInfo
+ @brief Represents the provider user info part of the response from the getAccountInfo endpoint.
+ @see https://developers.google.com/identity/toolkit/web/reference/relyingparty/getAccountInfo
+ */
+@interface FIRGetAccountInfoResponseProviderUserInfo : NSObject
+
+/** @property providerID
+ @brief The ID of the identity provider.
+ */
+@property(nonatomic, strong, readonly, nullable) NSString *providerID;
+
+/** @property displayName
+ @brief The user's display name at the identity provider.
+ */
+@property(nonatomic, strong, readonly, nullable) NSString *displayName;
+
+/** @property photoURL
+ @brief The user's photo URL at the identity provider.
+ */
+@property(nonatomic, strong, readonly, nullable) NSURL *photoURL;
+
+/** @property federatedID
+ @brief The user's identifier at the identity provider.
+ */
+@property(nonatomic, strong, readonly, nullable) NSString *federatedID;
+
+/** @property email
+ @brief The user's email at the identity provider.
+ */
+@property(nonatomic, strong, readonly, nullable) NSString *email;
+
+/** @property phoneNumber
+ @brief A phone number associated with the user.
+ */
+@property(nonatomic, readonly, nullable) NSString *phoneNumber;
+
+/** @fn init
+ @brief Please use initWithDictionary:
+ */
+- (instancetype)init NS_UNAVAILABLE;
+
+/** @fn initWithAPIKey:
+ @brief Designated initializer.
+ @param dictionary The provider user info data from endpoint.
+ */
+- (instancetype)initWithDictionary:(NSDictionary *)dictionary NS_DESIGNATED_INITIALIZER;
+
+@end
+
+/** @class FIRGetAccountInfoResponseUser
+ @brief Represents the firebase user info part of the response from the getAccountInfo endpoint.
+ @see https://developers.google.com/identity/toolkit/web/reference/relyingparty/getAccountInfo
+ */
+@interface FIRGetAccountInfoResponseUser : NSObject
+
+/** @property localID
+ @brief The ID of the user.
+ */
+@property(nonatomic, strong, readonly, nullable) NSString *localID;
+
+/** @property email
+ @brief The email or the user.
+ */
+@property(nonatomic, strong, readonly, nullable) NSString *email;
+
+/** @property emailVerified
+ @brief Whether the email has been verified.
+ */
+@property(nonatomic, assign, readonly) BOOL emailVerified;
+
+/** @property displayName
+ @brief The display name of the user.
+ */
+@property(nonatomic, strong, readonly, nullable) NSString *displayName;
+
+/** @property photoURL
+ @brief The user's photo URL.
+ */
+@property(nonatomic, strong, readonly, nullable) NSURL *photoURL;
+
+/** @property providerUserInfo
+ @brief The user's profiles at the associated identity providers.
+ */
+@property(nonatomic, strong, readonly, nullable)
+ NSArray<FIRGetAccountInfoResponseProviderUserInfo *> *providerUserInfo;
+
+/** @property passwordHash
+ @brief Information about user's password.
+ @remarks This is not necessarily the hash of user's actual password.
+ */
+@property(nonatomic, strong, readonly, nullable) NSString *passwordHash;
+
+/** @property phoneNumber
+ @brief A phone number associated with the user.
+ */
+@property(nonatomic, readonly, nullable) NSString *phoneNumber;
+
+/** @fn init
+ @brief Please use initWithDictionary:
+ */
+- (instancetype)init NS_UNAVAILABLE;
+
+/** @fn initWithAPIKey:
+ @brief Designated initializer.
+ @param dictionary The provider user info data from endpoint.
+ */
+- (instancetype)initWithDictionary:(NSDictionary *)dictionary NS_DESIGNATED_INITIALIZER;
+
+@end
+
+/** @class FIRGetAccountInfoResponse
+ @brief Represents the response from the setAccountInfo endpoint.
+ @see https://developers.google.com/identity/toolkit/web/reference/relyingparty/getAccountInfo
+ */
+@interface FIRGetAccountInfoResponse : NSObject <FIRAuthRPCResponse>
+
+/** @property providerUserInfo
+ @brief The requested users' profiles.
+ */
+@property(nonatomic, strong, readonly, nullable) NSArray<FIRGetAccountInfoResponseUser *> *users;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/RPCs/FIRGetAccountInfoResponse.m b/Firebase/Auth/Source/RPCs/FIRGetAccountInfoResponse.m
new file mode 100644
index 0000000..ba10746
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRGetAccountInfoResponse.m
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRGetAccountInfoResponse.h"
+
+#import "../Private/FIRAuthErrorUtils.h"
+
+/** @var kErrorKey
+ @brief The key for the "error" value in JSON responses from the server.
+ */
+static NSString *const kErrorKey = @"error";
+
+@implementation FIRGetAccountInfoResponseProviderUserInfo
+
+- (instancetype)initWithDictionary:(NSDictionary *)dictionary {
+ self = [super init];
+ if (self) {
+ _providerID = [dictionary[@"providerId"] copy];
+ _displayName = [dictionary[@"displayName"] copy];
+ NSString *photoURL = dictionary[@"photoUrl"];
+ if (photoURL) {
+ _photoURL = [NSURL URLWithString:photoURL];
+ }
+ _federatedID = [dictionary[@"federatedId"] copy];
+ _email = [dictionary[@"email"] copy];
+ _phoneNumber = [dictionary[@"phoneNumber"] copy];
+ }
+ return self;
+}
+
+@end
+
+@implementation FIRGetAccountInfoResponseUser
+
+- (instancetype)initWithDictionary:(NSDictionary *)dictionary {
+ self = [super init];
+ if (self) {
+ NSArray<NSDictionary *> *providerUserInfoData = dictionary[@"providerUserInfo"];
+ if (providerUserInfoData) {
+ NSMutableArray<FIRGetAccountInfoResponseProviderUserInfo *> *providerUserInfoArray =
+ [NSMutableArray arrayWithCapacity:providerUserInfoData.count];
+ for (NSDictionary *dictionary in providerUserInfoData) {
+ [providerUserInfoArray addObject:
+ [[FIRGetAccountInfoResponseProviderUserInfo alloc] initWithDictionary:dictionary]];
+ }
+ _providerUserInfo = [providerUserInfoArray copy];
+ }
+ _localID = [dictionary[@"localId"] copy];
+ _displayName = [dictionary[@"displayName"] copy];
+ _email = [dictionary[@"email"] copy];
+ NSString *photoURL = dictionary[@"photoUrl"];
+ if (photoURL) {
+ _photoURL = [NSURL URLWithString:photoURL];
+ }
+ _emailVerified = [dictionary[@"emailVerified"] boolValue];
+ _passwordHash = [dictionary[@"passwordHash"] copy];
+ _phoneNumber = [dictionary[@"phoneNumber"] copy];
+ }
+ return self;
+}
+
+@end
+
+@implementation FIRGetAccountInfoResponse
+
+- (BOOL)setWithDictionary:(NSDictionary *)dictionary
+ error:(NSError *_Nullable *_Nullable)error {
+ NSArray<NSDictionary *> *usersData = dictionary[@"users"];
+ // The client side never sends a getAccountInfo request with multiple localID, so only one user
+ // data is expected in the response.
+ if (![usersData isKindOfClass:[NSArray class]] || usersData.count != 1) {
+ if (error) {
+ *error = [FIRAuthErrorUtils unexpectedResponseWithDeserializedResponse:dictionary];
+ }
+ return NO;
+ }
+ _users = @[ [[FIRGetAccountInfoResponseUser alloc] initWithDictionary:usersData.firstObject] ];
+ return YES;
+}
+
+@end
diff --git a/Firebase/Auth/Source/RPCs/FIRGetOOBConfirmationCodeRequest.h b/Firebase/Auth/Source/RPCs/FIRGetOOBConfirmationCodeRequest.h
new file mode 100644
index 0000000..08ab495
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRGetOOBConfirmationCodeRequest.h
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+#import "FIRAuthRPCRequest.h"
+#import "FIRIdentityToolkitRequest.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @enum FIRGetOOBConfirmationCodeRequestType
+ @brief Types of OOB Confirmation Code requests.
+ */
+typedef NS_ENUM(NSInteger, FIRGetOOBConfirmationCodeRequestType) {
+ /** @var FIRGetOOBConfirmationCodeRequestTypePasswordReset
+ @brief Requests a password reset code.
+ */
+ FIRGetOOBConfirmationCodeRequestTypePasswordReset,
+
+ /** @var FIRGetOOBConfirmationCodeRequestTypeVerifyEmail
+ @brief Requests an email verification code.
+ */
+ FIRGetOOBConfirmationCodeRequestTypeVerifyEmail,
+};
+
+/** @enum FIRGetOOBConfirmationCodeRequest
+ @brief Represents the parameters for the getOOBConfirmationCode endpoint.
+ */
+@interface FIRGetOOBConfirmationCodeRequest : FIRIdentityToolkitRequest <FIRAuthRPCRequest>
+
+/** @property requestType
+ @brief The types of OOB Confirmation Code to request.
+ */
+@property(nonatomic, assign, readonly) FIRGetOOBConfirmationCodeRequestType requestType;
+
+/** @property email
+ @brief The email of the user.
+ @remarks For password reset.
+ */
+@property(nonatomic, copy, nullable, readonly) NSString *email;
+
+/** @property accessToken
+ @brief The STS Access Token of the authenticated user.
+ @remarks For email change.
+ */
+@property(nonatomic, copy, nullable, readonly) NSString *accessToken;
+
+/** @fn passwordResetRequestWithEmail:APIKey:
+ @brief Creates a password reset request.
+ @param email The user's email address.
+ @param APIKey The client's API Key.
+ @return A password reset request.
+ */
++ (nullable FIRGetOOBConfirmationCodeRequest *)passwordResetRequestWithEmail:(NSString *)email
+ APIKey:(NSString *)APIKey;
+
+/** @fn verifyEmailRequestWithAccessToken:APIKey:
+ @brief Creates a password reset request.
+ @param accessToken The user's STS Access Token.
+ @param APIKey The client's API Key.
+ @return A password reset request.
+ */
++ (nullable FIRGetOOBConfirmationCodeRequest *)
+ verifyEmailRequestWithAccessToken:(NSString *)accessToken APIKey:(NSString *)APIKey;
+
+/** @fn init
+ @brief Please use a factory method.
+ */
+- (nullable instancetype)initWithEndpoint:(NSString *)endpoint
+ APIKey:(NSString *)APIKey NS_UNAVAILABLE;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/RPCs/FIRGetOOBConfirmationCodeRequest.m b/Firebase/Auth/Source/RPCs/FIRGetOOBConfirmationCodeRequest.m
new file mode 100644
index 0000000..b0523e4
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRGetOOBConfirmationCodeRequest.m
@@ -0,0 +1,135 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRGetOOBConfirmationCodeRequest.h"
+
+#import "../Private/FIRAuthErrorUtils.h"
+#import "../Private/FIRAuth_Internal.h"
+
+/** @var kEndpoint
+ @brief The getOobConfirmationCode endpoint name.
+ */
+static NSString *const kEndpoint = @"getOobConfirmationCode";
+
+/** @var kRequestTypeKey
+ @brief The name of the required "requestType" property in the request.
+ */
+static NSString *const kRequestTypeKey = @"requestType";
+
+/** @var kEmailKey
+ @brief The name of the "email" property in the request.
+ */
+static NSString *const kEmailKey = @"email";
+
+/** @var kIDTokenKey
+ @brief The key for the "idToken" value in the request. This is actually the STS Access Token,
+ despite it's confusing (backwards compatiable) parameter name.
+ */
+static NSString *const kIDTokenKey = @"idToken";
+
+/** @var kPasswordResetRequestTypeValue
+ @brief The value for the "PASSWORD_RESET" request type.
+ */
+static NSString *const kPasswordResetRequestTypeValue = @"PASSWORD_RESET";
+
+/** @var kVerifyEmailRequestTypeValue
+ @brief The value for the "VERIFY_EMAIL" request type.
+ */
+static NSString *const kVerifyEmailRequestTypeValue = @"VERIFY_EMAIL";
+
+@interface FIRGetOOBConfirmationCodeRequest ()
+
+/** @fn initWithRequestType:email:APIKey:
+ @brief Designated initializer.
+ @param requestType The types of OOB Confirmation Code to request.
+ @param email The email of the user.
+ @param accessToken The STS Access Token of the currently signed in user.
+ @param APIKey The client's API Key.
+ */
+- (nullable instancetype)initWithRequestType:(FIRGetOOBConfirmationCodeRequestType)requestType
+ email:(nullable NSString *)email
+ accessToken:(nullable NSString *)accessToken
+ APIKey:(nullable NSString *)APIKey
+ NS_DESIGNATED_INITIALIZER;
+
+@end
+
+@implementation FIRGetOOBConfirmationCodeRequest
+
+/** @var requestTypeStringValueForRequestType:
+ @brief Returns the string equivilent for an @c FIRGetOOBConfirmationCodeRequestType value.
+ */
++ (NSString *)requestTypeStringValueForRequestType:
+ (FIRGetOOBConfirmationCodeRequestType)requestType {
+ switch (requestType) {
+ case FIRGetOOBConfirmationCodeRequestTypePasswordReset:
+ return kPasswordResetRequestTypeValue;
+ case FIRGetOOBConfirmationCodeRequestTypeVerifyEmail:
+ return kVerifyEmailRequestTypeValue;
+ // No default case so that we get a compiler warning if a new value was added to the enum.
+ }
+}
+
++ (FIRGetOOBConfirmationCodeRequest *)passwordResetRequestWithEmail:(NSString *)email
+ APIKey:(NSString *)APIKey {
+ return [[self alloc] initWithRequestType:FIRGetOOBConfirmationCodeRequestTypePasswordReset
+ email:email
+ accessToken:nil
+ APIKey:APIKey];
+}
+
++ (FIRGetOOBConfirmationCodeRequest *)
+ verifyEmailRequestWithAccessToken:(NSString *)accessToken APIKey:(NSString *)APIKey {
+ return [[self alloc] initWithRequestType:FIRGetOOBConfirmationCodeRequestTypeVerifyEmail
+ email:nil
+ accessToken:accessToken
+ APIKey:APIKey];
+}
+
+- (nullable instancetype)initWithRequestType:(FIRGetOOBConfirmationCodeRequestType)requestType
+ email:(nullable NSString *)email
+ accessToken:(nullable NSString *)accessToken
+ APIKey:(nullable NSString *)APIKey {
+ self = [super initWithEndpoint:kEndpoint APIKey:APIKey];
+ if (self) {
+ _requestType = requestType;
+ _email = email;
+ _accessToken = accessToken;
+ }
+ return self;
+}
+
+- (nullable id)unencodedHTTPRequestBodyWithError:(NSError *_Nullable *_Nullable)error {
+ NSMutableDictionary *body = [@{
+ kRequestTypeKey : [[self class] requestTypeStringValueForRequestType:_requestType]
+ } mutableCopy];
+
+ // For password reset requests, we only need an email address in addition to the already required
+ // fields.
+ if (_requestType == FIRGetOOBConfirmationCodeRequestTypePasswordReset) {
+ body[kEmailKey] = _email;
+ }
+
+ // For verify email requests, we only need an STS Access Token in addition to the already required
+ // fields.
+ if (_requestType == FIRGetOOBConfirmationCodeRequestTypeVerifyEmail) {
+ body[kIDTokenKey] = _accessToken;
+ }
+
+ return body;
+}
+
+@end
diff --git a/Firebase/Auth/Source/RPCs/FIRGetOOBConfirmationCodeResponse.h b/Firebase/Auth/Source/RPCs/FIRGetOOBConfirmationCodeResponse.h
new file mode 100644
index 0000000..69aa458
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRGetOOBConfirmationCodeResponse.h
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+#import "FIRAuthRPCResponse.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @class FIRGetOOBConfirmationCodeResponse
+ @brief Represents the response from the getOobConfirmationCode endpoint.
+ */
+@interface FIRGetOOBConfirmationCodeResponse : NSObject <FIRAuthRPCResponse>
+
+/** @property OOBCode
+ @brief The OOB code returned by the server in some cases.
+ */
+@property(nonatomic, copy, readonly, nullable) NSString *OOBCode;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/RPCs/FIRGetOOBConfirmationCodeResponse.m b/Firebase/Auth/Source/RPCs/FIRGetOOBConfirmationCodeResponse.m
new file mode 100644
index 0000000..d5fc1dd
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRGetOOBConfirmationCodeResponse.m
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRGetOOBConfirmationCodeResponse.h"
+
+#import "../Private/FIRAuthErrorUtils.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @var kOOBCodeKey
+ @brief The name of the field in the response JSON for the OOB code.
+ */
+static NSString *const kOOBCodeKey = @"oobCode";
+
+@implementation FIRGetOOBConfirmationCodeResponse
+
+- (BOOL)setWithDictionary:(NSDictionary *)dictionary
+ error:(NSError *_Nullable *_Nullable)error {
+ _OOBCode = [dictionary[kOOBCodeKey] copy];
+ return YES;
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/RPCs/FIRIdentityToolkitRequest.h b/Firebase/Auth/Source/RPCs/FIRIdentityToolkitRequest.h
new file mode 100644
index 0000000..873788d
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRIdentityToolkitRequest.h
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @class FIRIdentityToolkitRequest
+ @brief Represents a request to an identity toolkit endpoint.
+ */
+@interface FIRIdentityToolkitRequest : NSObject
+
+/** @property endpoint
+ @brief Gets the RPC's endpoint.
+ */
+@property(nonatomic, copy, readonly) NSString *endpoint;
+
+/** @property APIKey
+ @brief Gets the client's API key used for the request.
+ */
+@property(nonatomic, copy, readonly) NSString *APIKey;
+
+/** @fn init
+ @brief Please use initWithEndpoint:APIKey:
+ */
+- (instancetype)init NS_UNAVAILABLE;
+
+/** @fn initWithEndpoint:APIKey:
+ @brief Designated initializer.
+ @param endpoint The endpoint name.
+ @param APIKey The client's API Key.
+ */
+- (nullable instancetype)initWithEndpoint:(NSString *)endpoint
+ APIKey:(NSString *)APIKey
+ NS_DESIGNATED_INITIALIZER;
+
+/** @fn requestURL
+ @brief Gets the request's full URL.
+ */
+- (NSURL *)requestURL;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/RPCs/FIRIdentityToolkitRequest.m b/Firebase/Auth/Source/RPCs/FIRIdentityToolkitRequest.m
new file mode 100644
index 0000000..fb51a82
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRIdentityToolkitRequest.m
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRIdentityToolkitRequest.h"
+
+/** @var kAPIURLFormat
+ @brief URL format for server API calls.
+ */
+static NSString *const kAPIURLFormat = @"https://%@/identitytoolkit/v3/relyingparty/%@?key=%@";
+
+/** @var gAPIHost
+ @brief Host for server API calls.
+ */
+static NSString *gAPIHost = @"www.googleapis.com";
+
+@implementation FIRIdentityToolkitRequest
+
+- (nullable instancetype)initWithEndpoint:(NSString *)endpoint
+ APIKey:(NSString *)APIKey {
+ self = [super init];
+ if (self) {
+ _endpoint = [endpoint copy];
+ _APIKey = [APIKey copy];
+ }
+ return self;
+}
+
+- (NSURL *)requestURL {
+ NSString *URLString = [NSString stringWithFormat:kAPIURLFormat, gAPIHost, _endpoint, _APIKey];
+ NSURL *URL = [NSURL URLWithString:URLString];
+ return URL;
+}
+
+#pragma mark - Internal API for development
+
++ (NSString *)host {
+ return gAPIHost;
+}
+
++ (void)setHost:(NSString *)host {
+ gAPIHost = host;
+}
+
+@end
diff --git a/Firebase/Auth/Source/RPCs/FIRResetPasswordRequest.h b/Firebase/Auth/Source/RPCs/FIRResetPasswordRequest.h
new file mode 100644
index 0000000..66b03ad
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRResetPasswordRequest.h
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+#import "FIRAuthRPCRequest.h"
+#import "FIRIdentityToolkitRequest.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FIRResetPasswordRequest : FIRIdentityToolkitRequest <FIRAuthRPCRequest>
+
+/** @property oobCode
+ @brief The oobCode sent in the request.
+ */
+@property(nonatomic, copy, readonly) NSString *oobCode;
+
+/** @property updatedPassword
+ @brief The new password sent in the request.
+ */
+@property(nonatomic, copy, readonly) NSString *updatedPassword;
+
+/** @fn initWithEndpoint:APIKey:
+ @brief Please use initWithOOBCode:oobCode: instead.
+ */
+- (nullable instancetype)initWithEndpoint:(NSString *)endpoint
+ APIKey:(NSString *)APIKey NS_UNAVAILABLE;
+
+/** @fn initWithAPIKey:oobCode:currentPassword:
+ @brief Designated initializer.
+ @param APIKey The client's API Key.
+ @param oobCode The OOB Code.
+ @param newPassword The new password.
+ */
+- (nullable instancetype)initWithAPIKey:(NSString *)APIKey
+ oobCode:(NSString *)oobCode
+ newPassword:(nullable NSString *)newPassword NS_DESIGNATED_INITIALIZER;
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/RPCs/FIRResetPasswordRequest.m b/Firebase/Auth/Source/RPCs/FIRResetPasswordRequest.m
new file mode 100644
index 0000000..603aa00
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRResetPasswordRequest.m
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRResetPasswordRequest.h"
+
+/** @var kResetPasswordEndpoint
+ @brief The "resetPassword" endpoint.
+ */
+static NSString *const kResetPasswordEndpoint = @"resetPassword";
+
+/** @var kOOBCodeKey
+ @brief The "resetPassword" key.
+ */
+static NSString *const kOOBCodeKey = @"oobCode";
+
+/** @var kCurrentPasswordKey
+ @brief The "newPassword" key.
+ */
+static NSString *const kCurrentPasswordKey = @"newPassword";
+
+@implementation FIRResetPasswordRequest
+
+- (instancetype)initWithAPIKey:(NSString *)APIKey
+ oobCode:(NSString *)oobCode
+ newPassword:(NSString *)newPassword {
+ self = [super initWithEndpoint:kResetPasswordEndpoint APIKey:APIKey];
+ if (self) {
+ _oobCode = oobCode;
+ _updatedPassword = newPassword;
+ }
+ return self;
+}
+
+- (nullable id)unencodedHTTPRequestBodyWithError:(NSError *_Nullable *_Nullable)error {
+ NSMutableDictionary *postBody = [NSMutableDictionary dictionary];
+ postBody[kOOBCodeKey] = _oobCode;
+ if (_updatedPassword) {
+ postBody[kCurrentPasswordKey] = _updatedPassword;
+ }
+ return postBody;
+}
+
+@end
diff --git a/Firebase/Auth/Source/RPCs/FIRResetPasswordResponse.h b/Firebase/Auth/Source/RPCs/FIRResetPasswordResponse.h
new file mode 100644
index 0000000..28eb5f4
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRResetPasswordResponse.h
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+#import "FIRAuthRPCResponse.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @class FIRAuthResetPasswordResponse
+ @brief Represents the response from the resetPassword endpoint.
+ @remarks Possible error codes:
+ - FIRAuthErrorCodeWeakPassword
+ - FIRAuthErrorCodeUserDisabled
+ - FIRAuthErrorCodeOperationNotAllowed
+ - FIRAuthErrorCodeExpiredActionCode
+ - FIRAuthErrorCodeInvalidActionCode
+ @see https://developers.google.com/identity/toolkit/web/reference/relyingparty/resetPassword
+ */
+@interface FIRResetPasswordResponse : NSObject<FIRAuthRPCResponse>
+
+/** @property email
+ @brief The email address corresponding to the reset password request.
+ */
+@property(nonatomic, strong, readonly) NSString *email;
+
+/** @property verifiedEmail
+ @brief The verified email returned from the backend.
+ */
+@property(nonatomic, strong, readonly) NSString *verifiedEmail;
+
+/** @property requestType
+ @brief The tpye of request as returned by the backend.
+ */
+@property(nonatomic, strong, readonly) NSString *requestType;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/RPCs/FIRResetPasswordResponse.m b/Firebase/Auth/Source/RPCs/FIRResetPasswordResponse.m
new file mode 100644
index 0000000..d306f6c
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRResetPasswordResponse.m
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRResetPasswordResponse.h"
+
+#import "../Private/FIRAuthErrorUtils.h"
+
+@implementation FIRResetPasswordResponse
+
+- (BOOL)setWithDictionary:(NSDictionary *)dictionary
+ error:(NSError *_Nullable *_Nullable)error {
+ _email = [dictionary[@"email"] copy];
+ _requestType = [dictionary[@"requestType"] copy];
+ _verifiedEmail = [dictionary[@"newEmail"] copy];
+ return YES;
+}
+
+@end
diff --git a/Firebase/Auth/Source/RPCs/FIRSecureTokenRequest.h b/Firebase/Auth/Source/RPCs/FIRSecureTokenRequest.h
new file mode 100644
index 0000000..44c16b1
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRSecureTokenRequest.h
@@ -0,0 +1,109 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+#import "FIRAuthRPCRequest.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @enum FIRSecureTokenRequestGrantType
+ @brief Represents the possible grant types for a token request.
+ */
+typedef NS_ENUM(NSUInteger, FIRSecureTokenRequestGrantType) {
+ /** @var FIRSecureTokenRequestGrantTypeAuthorizationCode
+ @brief Indicates an authorization code request.
+ @remarks Exchanges a Gitkit "ID Token" for an STS Access Token and Refresh Token.
+ */
+ FIRSecureTokenRequestGrantTypeAuthorizationCode,
+
+ /** @var FIRSecureTokenRequestGrantTypeRefreshToken
+ @brief Indicates an refresh token request.
+ @remarks Uses an existing Refresh Token to create a new Access Token.
+ */
+ FIRSecureTokenRequestGrantTypeRefreshToken,
+};
+
+/** @class FIRSecureTokenRequest
+ @brief Represents the parameters for the token endpoint.
+ */
+@interface FIRSecureTokenRequest : NSObject <FIRAuthRPCRequest>
+
+/** @property grantType
+ @brief The type of grant requested.
+ @see FIRSecureTokenRequestGrantType
+ */
+@property(nonatomic, assign, readonly) FIRSecureTokenRequestGrantType grantType;
+
+/** @property scope
+ @brief The scopes requested (a comma-delimited list of scope strings.)
+ */
+@property(nonatomic, copy, readonly, nullable) NSString *scope;
+
+/** @property refreshToken
+ @brief The client's refresh token.
+ */
+@property(nonatomic, copy, readonly, nullable) NSString *refreshToken;
+
+/** @property code
+ @brief The client's authorization code (legacy Gitkit "ID Token").
+ */
+@property(nonatomic, copy, readonly, nullable) NSString *code;
+
+/** @property APIKey
+ @brief The client's API Key.
+ */
+@property(nonatomic, copy, readonly) NSString *APIKey;
+
+/** @fn authCodeRequestWithCode:
+ @brief Creates an authorization code request with the given code (legacy Gitkit "ID Token").
+ @param code The authorization code (legacy Gitkit "ID Token").
+ @param APIKey The client's API Key.
+ @return An authorization request.
+ */
++ (FIRSecureTokenRequest *)authCodeRequestWithCode:(NSString *)code APIKey:(NSString *)APIKey;
+
+/** @fn refreshRequestWithCode:
+ @brief Creates a refresh request with the given refresh token.
+ @param refreshToken The refresh token.
+ @param APIKey The client's API Key.
+ @return A refresh request.
+ */
++ (FIRSecureTokenRequest *)refreshRequestWithRefreshToken:(NSString *)refreshToken
+ APIKey:(NSString *)APIKey;
+
+/** @fn init
+ @brief Please use initWithGrantType:scope:refreshToken:code:
+ */
+- (instancetype)init NS_UNAVAILABLE;
+
+/** @fn initWithGrantType:scope:refreshToken:code:APIKey:
+ @brief Designated initializer.
+ @param grantType The type of request.
+ @param scope The scopes requested.
+ @param refreshToken The client's refresh token (for refresh requests.)
+ @param code The client's authorization code (Gitkit ID Token) (for authorization code requests.)
+ @param APIKey The client's API Key.
+ */
+- (nullable instancetype)initWithGrantType:(FIRSecureTokenRequestGrantType)grantType
+ scope:(nullable NSString *)scope
+ refreshToken:(nullable NSString *)refreshToken
+ code:(nullable NSString *)code
+ APIKey:(NSString *)APIKey NS_DESIGNATED_INITIALIZER;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/RPCs/FIRSecureTokenRequest.m b/Firebase/Auth/Source/RPCs/FIRSecureTokenRequest.m
new file mode 100644
index 0000000..1983542
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRSecureTokenRequest.m
@@ -0,0 +1,141 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRSecureTokenRequest.h"
+
+/** @var kFIRSecureTokenServiceGetTokenURLFormat
+ @brief The format of the secure token service URLs. Requires string format substitution with
+ the client's API Key.
+ */
+static NSString *const kFIRSecureTokenServiceGetTokenURLFormat = @"https://%@/v1/token?key=%@";
+
+/** @var kFIRSecureTokenServiceGrantTypeRefreshToken
+ @brief The string value of the @c FIRSecureTokenRequestGrantTypeRefreshToken request type.
+ */
+static NSString *const kFIRSecureTokenServiceGrantTypeRefreshToken = @"refresh_token";
+
+/** @var kFIRSecureTokenServiceGrantTypeAuthorizationCode
+ @brief The string value of the @c FIRSecureTokenRequestGrantTypeAuthorizationCode request type.
+ */
+static NSString *const kFIRSecureTokenServiceGrantTypeAuthorizationCode = @"authorization_code";
+
+/** @var kGrantTypeKey
+ @brief The key for the "grantType" parameter in the request.
+ */
+static NSString *const kGrantTypeKey = @"grantType";
+
+/** @var kScopeKey
+ @brief The key for the "scope" parameter in the request.
+ */
+static NSString *const kScopeKey = @"scope";
+
+/** @var kRefreshTokenKey
+ @brief The key for the "refreshToken" parameter in the request.
+ */
+static NSString *const kRefreshTokenKey = @"refreshToken";
+
+/** @var kCodeKey
+ @brief The key for the "code" parameter in the request.
+ */
+static NSString *const kCodeKey = @"code";
+
+/** @var gAPIHost
+ @brief Host for server API calls.
+ */
+static NSString *gAPIHost = @"securetoken.googleapis.com";
+
+@implementation FIRSecureTokenRequest
+
++ (FIRSecureTokenRequest *)authCodeRequestWithCode:(NSString *)code APIKey:(NSString *)APIKey {
+ return [[self alloc] initWithGrantType:FIRSecureTokenRequestGrantTypeAuthorizationCode
+ scope:nil
+ refreshToken:nil
+ code:code
+ APIKey:APIKey];
+}
+
++ (FIRSecureTokenRequest *)refreshRequestWithRefreshToken:(NSString *)refreshToken
+ APIKey:(NSString *)APIKey {
+ return [[self alloc] initWithGrantType:FIRSecureTokenRequestGrantTypeRefreshToken
+ scope:nil
+ refreshToken:refreshToken
+ code:nil
+ APIKey:APIKey];
+}
+
+/** @fn grantTypeStringWithGrantType:
+ @brief Converts a @c FIRSecureTokenRequestGrantType to it's @c NSString equivilent.
+ */
++ (NSString *)grantTypeStringWithGrantType:(FIRSecureTokenRequestGrantType)grantType {
+ switch (grantType) {
+ case FIRSecureTokenRequestGrantTypeAuthorizationCode:
+ return kFIRSecureTokenServiceGrantTypeAuthorizationCode;
+ case FIRSecureTokenRequestGrantTypeRefreshToken:
+ return kFIRSecureTokenServiceGrantTypeRefreshToken;
+ // No Default case so we will notice if new grant types are added to the enum.
+ }
+}
+
+- (nullable instancetype)initWithGrantType:(FIRSecureTokenRequestGrantType)grantType
+ scope:(nullable NSString *)scope
+ refreshToken:(nullable NSString *)refreshToken
+ code:(nullable NSString *)code
+ APIKey:(NSString *)APIKey {
+ self = [super init];
+ if (self) {
+ _grantType = grantType;
+ _scope = [scope copy];
+ _refreshToken = [refreshToken copy];
+ _code = [code copy];
+ _APIKey = [APIKey copy];
+ }
+ return self;
+}
+
+- (NSURL *)requestURL {
+ NSString *URLString =
+ [NSString stringWithFormat:kFIRSecureTokenServiceGetTokenURLFormat, gAPIHost, _APIKey];
+ NSURL *URL = [NSURL URLWithString:URLString];
+ return URL;
+}
+
+- (nullable id)unencodedHTTPRequestBodyWithError:(NSError *_Nullable *_Nullable)error {
+ NSMutableDictionary *postBody = [@{
+ kGrantTypeKey : [[self class] grantTypeStringWithGrantType:_grantType]
+ } mutableCopy];
+ if (_scope) {
+ postBody[kScopeKey] = _scope;
+ }
+ if (_refreshToken) {
+ postBody[kRefreshTokenKey] = _refreshToken;
+ }
+ if (_code) {
+ postBody[kCodeKey] = _code;
+ }
+ return postBody;
+}
+
+#pragma mark - Internal API for development
+
++ (NSString *)host {
+ return gAPIHost;
+}
+
++ (void)setHost:(NSString *)host {
+ gAPIHost = host;
+}
+
+@end
diff --git a/Firebase/Auth/Source/RPCs/FIRSecureTokenResponse.h b/Firebase/Auth/Source/RPCs/FIRSecureTokenResponse.h
new file mode 100644
index 0000000..0dd4a20
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRSecureTokenResponse.h
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+#import "FIRAuthRPCResponse.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @class FIRSecureTokenResponse
+ @brief Represents the response from the token endpoint.
+ */
+@interface FIRSecureTokenResponse : NSObject <FIRAuthRPCResponse>
+
+/** @property approximateExpirationDate
+ @brief The approximate expiration date of the access token.
+ */
+@property(nonatomic, copy, readonly, nullable) NSDate *approximateExpirationDate;
+
+/** @property refreshToken
+ @brief The refresh token. (Possibly an updated one for refresh requests.)
+ */
+@property(nonatomic, copy, readonly, nullable) NSString *refreshToken;
+
+/** @property accessToken
+ @brief The new access token.
+ */
+@property(nonatomic, copy, readonly, nullable) NSString *accessToken;
+
+/** @property IDToken
+ @brief The new ID Token.
+ */
+@property(nonatomic, copy, readonly, nullable) NSString *IDToken;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/RPCs/FIRSecureTokenResponse.m b/Firebase/Auth/Source/RPCs/FIRSecureTokenResponse.m
new file mode 100644
index 0000000..8ff3dde
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRSecureTokenResponse.m
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRSecureTokenResponse.h"
+
+#import "../Private/FIRAuthErrorUtils.h"
+
+/** @var kExpiresInKey
+ @brief The key for the number of seconds till the access token expires.
+ */
+static NSString *const kExpiresInKey = @"expires_in";
+
+/** @var kRefreshTokenKey
+ @brief The key for the refresh token.
+ */
+static NSString *const kRefreshTokenKey = @"refresh_token";
+
+/** @var kAccessTokenKey
+ @brief The key for the access token.
+ */
+static NSString *const kAccessTokenKey = @"access_token";
+
+/** @var kIDTokenKey
+ @brief The key for the "id_token" value in the response.
+ */
+static NSString *const kIDTokenKey = @"id_token";
+
+@implementation FIRSecureTokenResponse
+
+- (nullable NSString *)expectedKind {
+ return nil;
+}
+
+- (BOOL)setWithDictionary:(NSDictionary *)dictionary
+ error:(NSError *_Nullable *_Nullable)error {
+ _refreshToken = dictionary[kRefreshTokenKey];
+ _accessToken = dictionary[kAccessTokenKey];
+ _IDToken = dictionary[kIDTokenKey];
+ if (!_accessToken.length) {
+ if (error) {
+ *error = [FIRAuthErrorUtils unexpectedResponseWithDeserializedResponse:dictionary];
+ }
+ return NO;
+ }
+ id expiresIn = dictionary[kExpiresInKey];
+ if (![expiresIn isKindOfClass:[NSString class]]) {
+ if (error) {
+ *error = [FIRAuthErrorUtils unexpectedResponseWithDeserializedResponse:dictionary];
+ }
+ return NO;
+ }
+
+ _approximateExpirationDate = [NSDate dateWithTimeIntervalSinceNow:[expiresIn doubleValue]];
+ return YES;
+}
+
+@end
diff --git a/Firebase/Auth/Source/RPCs/FIRSendVerificationCodeRequest.h b/Firebase/Auth/Source/RPCs/FIRSendVerificationCodeRequest.h
new file mode 100644
index 0000000..596fb8c
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRSendVerificationCodeRequest.h
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRIdentityToolkitRequest.h"
+
+#import "FIRAuthRPCRequest.h"
+#import "FIRIdentityToolkitRequest.h"
+
+@class FIRAuthAppCredential;
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FIRSendVerificationCodeRequest : FIRIdentityToolkitRequest <FIRAuthRPCRequest>
+
+/** @property phoneNumber
+ @brief The phone number to which the verification code should be sent.
+ */
+@property(nonatomic, strong, readonly) NSString *phoneNumber;
+
+/** @property appCredential
+ @brief The credential to prove the identity of the app in order to send the verification code.
+ */
+@property(nonatomic, strong, readonly) FIRAuthAppCredential *appCredential;
+
+/** @fn initWithEndpoint:APIKey:
+ @brief Please use initWithPhoneNumber:APIKey:
+ */
+- (nullable instancetype)initWithEndpoint:(NSString *)endpoint
+ APIKey:(NSString *)APIKey NS_UNAVAILABLE;
+
+/** @fn initWithPhoneNumber:APIKey:
+ @brief Designated initializer.
+ @param phoneNumber The phone number to which the verification code is to be sent.
+ @param appCredential The credential that proves the identity of the app.
+ @param APIKey The client's API Key.
+ */
+- (nullable instancetype)initWithPhoneNumber:(NSString *)phoneNumber
+ appCredential:(FIRAuthAppCredential *)appCredential
+ APIKey:(NSString *)APIKey NS_DESIGNATED_INITIALIZER;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/RPCs/FIRSendVerificationCodeRequest.m b/Firebase/Auth/Source/RPCs/FIRSendVerificationCodeRequest.m
new file mode 100644
index 0000000..f2212ad
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRSendVerificationCodeRequest.m
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRSendVerificationCodeRequest.h"
+
+#import "../Private/FIRAuthAppCredential.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @var kSendVerificationCodeEndPoint
+ @brief The "sendVerificationCodeEnd" endpoint.
+ */
+static NSString *const kSendVerificationCodeEndPoint = @"sendVerificationCode";
+
+/** @var kPhoneNumberKey
+ @brief The key for the Phone Number parameter in the request.
+ */
+static NSString *const kPhoneNumberKey = @"phoneNumber";
+
+/** @var kReceiptKey
+ @brief The key for the receipt parameter in the request.
+ */
+static NSString *const kReceiptKey = @"iosReceipt";
+
+/** @var kSecretKey
+ @brief The key for the Secret parameter in the request.
+ */
+static NSString *const kSecretKey = @"iosSecret";
+
+@implementation FIRSendVerificationCodeRequest {
+}
+
+- (nullable instancetype)initWithPhoneNumber:(NSString *)phoneNumber
+ appCredential:(FIRAuthAppCredential *)appCredential
+ APIKey:(NSString *)APIKey {
+ self = [super initWithEndpoint:kSendVerificationCodeEndPoint APIKey:APIKey];
+ if (self) {
+ _phoneNumber = [phoneNumber copy];
+ _appCredential = appCredential;
+ }
+ return self;
+}
+
+- (nullable id)unencodedHTTPRequestBodyWithError:(NSError *_Nullable *_Nullable)error {
+ NSMutableDictionary *postBody = [NSMutableDictionary dictionary];
+ if (_phoneNumber) {
+ postBody[kPhoneNumberKey] = _phoneNumber;
+ }
+ if (_appCredential.receipt) {
+ postBody[kReceiptKey] = _appCredential.receipt;
+ }
+ if (_appCredential.secret) {
+ postBody[kSecretKey] = _appCredential.secret;
+ }
+ return postBody;
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/RPCs/FIRSendVerificationCodeResponse.h b/Firebase/Auth/Source/RPCs/FIRSendVerificationCodeResponse.h
new file mode 100644
index 0000000..1a49ec2
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRSendVerificationCodeResponse.h
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+#import "FIRAuthRPCResponse.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FIRSendVerificationCodeResponse : NSObject <FIRAuthRPCResponse>
+
+/** @property verificationID
+ @brief Encrypted session information returned by the backend.
+ */
+@property(nonatomic, readonly) NSString *verificationID;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/RPCs/FIRSendVerificationCodeResponse.m b/Firebase/Auth/Source/RPCs/FIRSendVerificationCodeResponse.m
new file mode 100644
index 0000000..9e47b6e
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRSendVerificationCodeResponse.m
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRSendVerificationCodeResponse.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@implementation FIRSendVerificationCodeResponse
+
+// TODO: remove when resolving b/37169084 .
+- (nullable NSString *)expectedKind {
+ return nil;
+}
+
+- (BOOL)setWithDictionary:(NSDictionary *)dictionary
+ error:(NSError *_Nullable *_Nullable)error {
+ _verificationID = [dictionary[@"sessionInfo"] copy];
+ return YES;
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/RPCs/FIRSetAccountInfoRequest.h b/Firebase/Auth/Source/RPCs/FIRSetAccountInfoRequest.h
new file mode 100644
index 0000000..4816474
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRSetAccountInfoRequest.h
@@ -0,0 +1,149 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+#import "FIRAuthRPCRequest.h"
+#import "FIRIdentityToolkitRequest.h"
+
+@class FIRGetAccountInfoResponse;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @var FIRSetAccountInfoUserAttributeEmail
+ @brief Constant for email attribute used in "deleteAttributes".
+ */
+extern NSString *const FIRSetAccountInfoUserAttributeEmail;
+
+/** @var FIRSetAccountInfoUserAttributeDisplayName
+ @brief Constant for displayName attribute used in "deleteAttributes".
+ */
+extern NSString *const FIRSetAccountInfoUserAttributeDisplayName;
+
+/** @var FIRSetAccountInfoUserAttributeProvider
+ @brief Constant for provider attribute used in "deleteAttributes".
+ */
+extern NSString *const FIRSetAccountInfoUserAttributeProvider;
+
+/** @var FIRSetAccountInfoUserAttributePhotoURL
+ @brief Constant for photoURL attribute used in "deleteAttributes".
+ */
+extern NSString *const FIRSetAccountInfoUserAttributePhotoURL;
+
+/** @var FIRSetAccountInfoUserAttributePassword
+ @brief Constant for password attribute used in "deleteAttributes".
+ */
+extern NSString *const FIRSetAccountInfoUserAttributePassword;
+
+/** @class FIRSetAccountInfoRequest
+ @brief Represents the parameters for the setAccountInfo endpoint.
+ @see https://developers.google.com/identity/toolkit/web/reference/relyingparty/setAccountInfo
+ */
+@interface FIRSetAccountInfoRequest : FIRIdentityToolkitRequest <FIRAuthRPCRequest>
+
+/** @property accessToken
+ @brief The STS Access Token of the authenticated user.
+ */
+@property(nonatomic, copy, nullable) NSString *accessToken;
+
+/** @property displayName
+ @brief The name of the user.
+ */
+@property(nonatomic, copy, nullable) NSString *displayName;
+
+/** @property localID
+ @brief The local ID of the user.
+ */
+@property(nonatomic, copy, nullable) NSString *localID;
+
+/** @property email
+ @brief The email of the user.
+ */
+@property(nonatomic, copy, nullable) NSString *email;
+
+/** @property photoURL
+ @brief The photoURL of the user.
+ */
+@property(nonatomic, copy, nullable) NSURL *photoURL;
+
+/** @property password
+ @brief The new password of the user.
+ */
+@property(nonatomic, copy, nullable) NSString *password;
+
+/** @property providers
+ @brief The associated identity providers of the user.
+ */
+@property(nonatomic, copy, nullable) NSArray<NSString *> *providers;
+
+/** @property OOBCode
+ @brief The out-of-band code of the change email request.
+ */
+@property(nonatomic, copy, nullable) NSString *OOBCode;
+
+/** @property emailVerified
+ @brief Whether to mark the email as verified or not.
+ */
+@property(nonatomic, assign) BOOL emailVerified;
+
+/** @property upgradeToFederatedLogin
+ @brief Whether to mark the user to upgrade to federated login.
+ */
+@property(nonatomic, assign) BOOL upgradeToFederatedLogin;
+
+/** @property captchaChallenge
+ @brief The captcha challenge.
+ */
+@property(nonatomic, copy, nullable) NSString *captchaChallenge;
+
+/** @property captchaResponse
+ @brief Response to the captcha.
+ */
+@property(nonatomic, copy, nullable) NSString *captchaResponse;
+
+/** @property deleteAttributes
+ @brief The list of user attributes to delete.
+ @remarks Every element of the list must be one of the predefined constant starts with
+ "FIRSetAccountInfoUserAttribute".
+ */
+@property(nonatomic, copy, nullable) NSArray<NSString *> *deleteAttributes;
+
+/** @property deleteProviders
+ @brief The list of identity providers to delete.
+ */
+@property(nonatomic, copy, nullable) NSArray<NSString *> *deleteProviders;
+
+/** @property returnSecureToken
+ @brief Whether the response should return access token and refresh token directly.
+ @remarks The default value is @c YES .
+ */
+@property(nonatomic, assign) BOOL returnSecureToken;
+
+/** @fn initWithEndpoint:APIKey:
+ @brief Please use initWithAPIKey:
+ */
+- (nullable instancetype)initWithEndpoint:(NSString *)endpoint
+ APIKey:(NSString *)APIKey NS_UNAVAILABLE;
+
+/** @fn initWithAPIKey:
+ @brief Designated initializer.
+ @param APIKey The client's API Key.
+ */
+- (nullable instancetype)initWithAPIKey:(NSString *)APIKey NS_DESIGNATED_INITIALIZER;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/RPCs/FIRSetAccountInfoRequest.m b/Firebase/Auth/Source/RPCs/FIRSetAccountInfoRequest.m
new file mode 100644
index 0000000..b18d0dd
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRSetAccountInfoRequest.m
@@ -0,0 +1,174 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRSetAccountInfoRequest.h"
+
+#import "../Private/FIRAuthErrorUtils.h"
+#import "../Private/FIRAuth_Internal.h"
+#import "FIRGetAccountInfoResponse.h"
+
+NSString *const FIRSetAccountInfoUserAttributeEmail = @"EMAIL";
+
+NSString *const FIRSetAccountInfoUserAttributeDisplayName = @"DISPLAY_NAME";
+
+NSString *const FIRSetAccountInfoUserAttributeProvider = @"PROVIDER";
+
+NSString *const FIRSetAccountInfoUserAttributePhotoURL = @"PHOTO_URL";
+
+NSString *const FIRSetAccountInfoUserAttributePassword = @"PASSWORD";
+
+/** @var kCreateAuthURIEndpoint
+ @brief The "setAccountInfo" endpoint.
+ */
+static NSString *const kSetAccountInfoEndpoint = @"setAccountInfo";
+
+/** @var kIDTokenKey
+ @brief The key for the "idToken" value in the request. This is actually the STS Access Token,
+ despite it's confusing (backwards compatiable) parameter name.
+ */
+static NSString *const kIDTokenKey = @"idToken";
+
+/** @var kDisplayNameKey
+ @brief The key for the "displayName" value in the request.
+ */
+static NSString *const kDisplayNameKey = @"displayName";
+
+/** @var kLocalIDKey
+ @brief The key for the "localID" value in the request.
+ */
+static NSString *const kLocalIDKey = @"localId";
+
+/** @var kEmailKey
+ @brief The key for the "email" value in the request.
+ */
+static NSString *const kEmailKey = @"email";
+
+/** @var kPasswordKey
+ @brief The key for the "password" value in the request.
+ */
+static NSString *const kPasswordKey = @"password";
+
+/** @var kPhotoURLKey
+ @brief The key for the "photoURL" value in the request.
+ */
+static NSString *const kPhotoURLKey = @"photoUrl";
+
+/** @var kProvidersKey
+ @brief The key for the "providers" value in the request.
+ */
+static NSString *const kProvidersKey = @"provider";
+
+/** @var kOOBCodeKey
+ @brief The key for the "OOBCode" value in the request.
+ */
+static NSString *const kOOBCodeKey = @"oobCode";
+
+/** @var kEmailVerifiedKey
+ @brief The key for the "emailVerified" value in the request.
+ */
+static NSString *const kEmailVerifiedKey = @"emailVerified";
+
+/** @var kUpgradeToFederatedLoginKey
+ @brief The key for the "upgradeToFederatedLogin" value in the request.
+ */
+static NSString *const kUpgradeToFederatedLoginKey = @"upgradeToFederatedLogin";
+
+/** @var kCaptchaChallengeKey
+ @brief The key for the "captchaChallenge" value in the request.
+ */
+static NSString *const kCaptchaChallengeKey = @"captchaChallenge";
+
+/** @var kCaptchaResponseKey
+ @brief The key for the "captchaResponse" value in the request.
+ */
+static NSString *const kCaptchaResponseKey = @"captchaResponse";
+
+/** @var kDeleteAttributesKey
+ @brief The key for the "deleteAttribute" value in the request.
+ */
+static NSString *const kDeleteAttributesKey = @"deleteAttribute";
+
+/** @var kDeleteProvidersKey
+ @brief The key for the "deleteProvider" value in the request.
+ */
+static NSString *const kDeleteProvidersKey = @"deleteProvider";
+
+/** @var kReturnSecureTokenKey
+ @brief The key for the "returnSecureToken" value in the request.
+ */
+static NSString *const kReturnSecureTokenKey = @"returnSecureToken";
+
+@implementation FIRSetAccountInfoRequest
+
+- (nullable instancetype)initWithAPIKey:(NSString *)APIKey {
+ self = [super initWithEndpoint:kSetAccountInfoEndpoint APIKey:APIKey];
+ if (self) {
+ _returnSecureToken = YES;
+ }
+ return self;
+}
+
+- (nullable id)unencodedHTTPRequestBodyWithError:(NSError *_Nullable *_Nullable)error {
+ NSMutableDictionary *postBody = [NSMutableDictionary dictionary];
+ if (_accessToken) {
+ postBody[kIDTokenKey] = _accessToken;
+ }
+ if (_displayName) {
+ postBody[kDisplayNameKey] = _displayName;
+ }
+ if (_localID) {
+ postBody[kLocalIDKey] = _localID;
+ }
+ if (_email) {
+ postBody[kEmailKey] = _email;
+ }
+ if (_password) {
+ postBody[kPasswordKey] = _password;
+ }
+ if (_photoURL) {
+ postBody[kPhotoURLKey] = _photoURL.absoluteString;
+ }
+ if (_providers) {
+ postBody[kProvidersKey] = _providers;
+ }
+ if (_OOBCode) {
+ postBody[kOOBCodeKey] = _OOBCode;
+ }
+ if (_emailVerified) {
+ postBody[kEmailVerifiedKey] = @YES;
+ }
+ if (_upgradeToFederatedLogin) {
+ postBody[kUpgradeToFederatedLoginKey] = @YES;
+ }
+ if (_captchaChallenge) {
+ postBody[kCaptchaChallengeKey] = _captchaChallenge;
+ }
+ if (_captchaResponse) {
+ postBody[kCaptchaResponseKey] = _captchaResponse;
+ }
+ if (_deleteAttributes) {
+ postBody[kDeleteAttributesKey] = _deleteAttributes;
+ }
+ if (_deleteProviders) {
+ postBody[kDeleteProvidersKey] = _deleteProviders;
+ }
+ if (_returnSecureToken) {
+ postBody[kReturnSecureTokenKey] = @YES;
+ }
+ return postBody;
+}
+
+@end
diff --git a/Firebase/Auth/Source/RPCs/FIRSetAccountInfoResponse.h b/Firebase/Auth/Source/RPCs/FIRSetAccountInfoResponse.h
new file mode 100644
index 0000000..92895c0
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRSetAccountInfoResponse.h
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+#import "FIRAuthRPCResponse.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @class FIRSetAccountInfoResponseProviderUserInfo
+ @brief Represents the provider user info part of the response from the setAccountInfo endpoint.
+ @see https://developers.google.com/identity/toolkit/web/reference/relyingparty/setAccountInfo
+ */
+@interface FIRSetAccountInfoResponseProviderUserInfo : NSObject
+
+/** @property providerID
+ @brief The ID of the identity provider.
+ */
+@property(nonatomic, strong, readonly, nullable) NSString *providerID;
+
+/** @property displayName
+ @brief The user's display name at the identity provider.
+ */
+@property(nonatomic, strong, readonly, nullable) NSString *displayName;
+
+/** @property photoURL
+ @brief The user's photo URL at the identity provider.
+ */
+@property(nonatomic, strong, readonly, nullable) NSURL *photoURL;
+
+/** @fn init
+ @brief Please use initWithDictionary:
+ */
+- (instancetype)init NS_UNAVAILABLE;
+
+/** @fn initWithAPIKey:
+ @brief Designated initializer.
+ @param dictionary The provider user info data from endpoint.
+ */
+- (instancetype)initWithDictionary:(NSDictionary *)dictionary NS_DESIGNATED_INITIALIZER;
+
+@end
+
+/** @class FIRSetAccountInfoResponse
+ @brief Represents the response from the setAccountInfo endpoint.
+ @see https://developers.google.com/identity/toolkit/web/reference/relyingparty/setAccountInfo
+ */
+@interface FIRSetAccountInfoResponse : NSObject <FIRAuthRPCResponse>
+
+/** @property email
+ @brief The email or the user.
+ */
+@property(nonatomic, strong, readonly, nullable) NSString *email;
+
+/** @property displayName
+ @brief The display name of the user.
+ */
+@property(nonatomic, strong, readonly, nullable) NSString *displayName;
+
+/** @property providerUserInfo
+ @brief The user's profiles at the associated identity providers.
+ */
+@property(nonatomic, strong, readonly, nullable)
+ NSArray<FIRSetAccountInfoResponseProviderUserInfo *> *providerUserInfo;
+
+/** @property IDToken
+ @brief Either an authorization code suitable for performing an STS token exchange, or the
+ access token from Secure Token Service, depending on whether @c returnSecureToken is set
+ on the request.
+ */
+@property(nonatomic, strong, readonly, nullable) NSString *IDToken;
+
+/** @property approximateExpirationDate
+ @brief The approximate expiration date of the access token.
+ */
+@property(nonatomic, copy, readonly, nullable) NSDate *approximateExpirationDate;
+
+/** @property refreshToken
+ @brief The refresh token from Secure Token Service.
+ */
+@property(nonatomic, strong, readonly, nullable) NSString *refreshToken;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/RPCs/FIRSetAccountInfoResponse.m b/Firebase/Auth/Source/RPCs/FIRSetAccountInfoResponse.m
new file mode 100644
index 0000000..140ba25
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRSetAccountInfoResponse.m
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRSetAccountInfoResponse.h"
+
+#import "../Private/FIRAuthErrorUtils.h"
+
+@implementation FIRSetAccountInfoResponseProviderUserInfo
+
+- (instancetype)initWithDictionary:(NSDictionary *)dictionary {
+ self = [super init];
+ if (self) {
+ _providerID = [dictionary[@"providerId"] copy];
+ _displayName = [dictionary[@"displayName"] copy];
+ NSString *photoURL = dictionary[@"photoUrl"];
+ if (photoURL) {
+ _photoURL = [NSURL URLWithString:photoURL];
+ }
+ }
+ return self;
+}
+
+@end
+
+@implementation FIRSetAccountInfoResponse
+
+- (BOOL)setWithDictionary:(NSDictionary *)dictionary
+ error:(NSError *_Nullable *_Nullable)error {
+ _email = [dictionary[@"email"] copy];
+ _displayName = [dictionary[@"displayName"] copy];
+ _IDToken = [dictionary[@"idToken"] copy];
+ _approximateExpirationDate = [dictionary[@"expiresIn"] isKindOfClass:[NSString class]] ?
+ [NSDate dateWithTimeIntervalSinceNow:[dictionary[@"expiresIn"] doubleValue]] : nil;
+ _refreshToken = [dictionary[@"refreshToken"] copy];
+ NSArray<NSDictionary *> *providerUserInfoData = dictionary[@"providerUserInfo"];
+ if (providerUserInfoData) {
+ NSMutableArray<FIRSetAccountInfoResponseProviderUserInfo *> *providerUserInfoArray =
+ [NSMutableArray arrayWithCapacity:providerUserInfoData.count];
+ for (NSDictionary *dictionary in providerUserInfoData) {
+ [providerUserInfoArray addObject:
+ [[FIRSetAccountInfoResponseProviderUserInfo alloc] initWithDictionary:dictionary]];
+ }
+ _providerUserInfo = [providerUserInfoArray copy];
+ }
+ return YES;
+}
+
+@end
diff --git a/Firebase/Auth/Source/RPCs/FIRSignUpNewUserRequest.h b/Firebase/Auth/Source/RPCs/FIRSignUpNewUserRequest.h
new file mode 100644
index 0000000..46b47d5
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRSignUpNewUserRequest.h
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+#import "FIRAuthRPCRequest.h"
+#import "FIRIdentityToolkitRequest.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FIRSignUpNewUserRequest : FIRIdentityToolkitRequest <FIRAuthRPCRequest>
+
+/** @property email
+ @brief The email of the user.
+ */
+@property(nonatomic, copy, nullable) NSString *email;
+
+/** @property password
+ @brief The password inputed by the user.
+ */
+@property(nonatomic, copy, nullable) NSString *password;
+
+/** @property displayName
+ @brief The password inputed by the user.
+ */
+@property(nonatomic, copy, nullable) NSString *displayName;
+
+/** @property returnSecureToken
+ @brief Whether the response should return access token and refresh token directly.
+ @remarks The default value is @c YES .
+ */
+@property(nonatomic, assign) BOOL returnSecureToken;
+
+/** @fn initWithEndpoint:APIKey:
+ @brief Please use initWithEmail:password:APIKey:
+ */
+- (nullable instancetype)initWithEndpoint:(NSString *)endpoint
+ APIKey:(NSString *)APIKey NS_UNAVAILABLE;
+/** @fn initWithAPIKey:
+ @brief initializer for anonymous sign-in.
+ */
+- (nullable instancetype)initWithAPIKey:(NSString *)APIKey;
+
+/** @fn initWithAPIKey:email:password:
+ @brief Designated initializer.
+ */
+- (nullable instancetype)initWithAPIKey:(NSString *)APIKey
+ email:(nullable NSString *)email
+ password:(nullable NSString *)password
+ displayName:(nullable NSString *)displayName NS_DESIGNATED_INITIALIZER;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/RPCs/FIRSignUpNewUserRequest.m b/Firebase/Auth/Source/RPCs/FIRSignUpNewUserRequest.m
new file mode 100644
index 0000000..af60b11
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRSignUpNewUserRequest.m
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRSignUpNewUserRequest.h"
+
+/** @var kSignupNewUserEndpoint
+ @brief The "SingupNewUserEndpoint" endpoint.
+ */
+static NSString *const kSignupNewUserEndpoint = @"signupNewUser";
+
+/** @var kEmailKey
+ @brief The key for the "email" value in the request.
+ */
+static NSString *const kEmailKey = @"email";
+
+/** @var kPasswordKey
+ @brief The key for the "password" value in the request.
+ */
+static NSString *const kPasswordKey = @"password";
+
+/** @var kDisplayNameKey
+ @brief The key for the "kDisplayName" value in the request.
+ */
+static NSString *const kDisplayNameKey = @"displayName";
+
+/** @var kReturnSecureTokenKey
+ @brief The key for the "returnSecureToken" value in the request.
+ */
+static NSString *const kReturnSecureTokenKey = @"returnSecureToken";
+
+@implementation FIRSignUpNewUserRequest
+
+- (nullable instancetype)initWithAPIKey:(NSString *)APIKey
+ email:(NSString *)email
+ password:(NSString *)password
+ displayName:(NSString *)displayName {
+ self = [super initWithEndpoint:kSignupNewUserEndpoint APIKey:APIKey];
+ if (self) {
+ _email = [email copy];
+ _password = [password copy];
+ _displayName = [displayName copy];
+ _returnSecureToken = YES;
+ }
+ return self;
+}
+
+- (nullable instancetype)initWithAPIKey:(NSString *)APIKey{
+ self = [self initWithAPIKey:APIKey email:nil password:nil displayName:nil];
+ return self;
+}
+
+- (nullable id)unencodedHTTPRequestBodyWithError:(NSError *_Nullable *_Nullable)error {
+ NSMutableDictionary *postBody = [NSMutableDictionary dictionary];
+ if (_email) {
+ postBody[kEmailKey] = _email;
+ }
+ if (_password) {
+ postBody[kPasswordKey] = _password;
+ }
+ if (_displayName) {
+ postBody[kDisplayNameKey] = _displayName;
+ }
+ if (_returnSecureToken) {
+ postBody[kReturnSecureTokenKey] = @YES;
+ }
+ return postBody;
+}
+
+@end
diff --git a/Firebase/Auth/Source/RPCs/FIRSignUpNewUserResponse.h b/Firebase/Auth/Source/RPCs/FIRSignUpNewUserResponse.h
new file mode 100644
index 0000000..0d55939
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRSignUpNewUserResponse.h
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+#import "FIRAuthRPCResponse.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FIRSignUpNewUserResponse : NSObject<FIRAuthRPCResponse>
+
+/** @property IDToken
+ @brief Either an authorization code suitable for performing an STS token exchange, or the
+ access token from Secure Token Service, depending on whether @c returnSecureToken is set
+ on the request.
+ */
+@property(nonatomic, strong, readonly, nullable) NSString *IDToken;
+
+/** @property approximateExpirationDate
+ @brief The approximate expiration date of the access token.
+ */
+@property(nonatomic, copy, readonly, nullable) NSDate *approximateExpirationDate;
+
+/** @property refreshToken
+ @brief The refresh token from Secure Token Service.
+ */
+@property(nonatomic, strong, readonly, nullable) NSString *refreshToken;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/RPCs/FIRSignUpNewUserResponse.m b/Firebase/Auth/Source/RPCs/FIRSignUpNewUserResponse.m
new file mode 100644
index 0000000..4196d21
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRSignUpNewUserResponse.m
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRSignUpNewUserResponse.h"
+
+#import "../Private/FIRAuthErrorUtils.h"
+
+@implementation FIRSignUpNewUserResponse
+
+- (BOOL)setWithDictionary:(NSDictionary *)dictionary
+ error:(NSError *_Nullable *_Nullable)error {
+ _IDToken = [dictionary[@"idToken"] copy];
+ _approximateExpirationDate = [dictionary[@"expiresIn"] isKindOfClass:[NSString class]] ?
+ [NSDate dateWithTimeIntervalSinceNow:[dictionary[@"expiresIn"] doubleValue]] : nil;
+ _refreshToken = [dictionary[@"refreshToken"] copy];
+ return YES;
+}
+
+@end
diff --git a/Firebase/Auth/Source/RPCs/FIRVerifyAssertionRequest.h b/Firebase/Auth/Source/RPCs/FIRVerifyAssertionRequest.h
new file mode 100644
index 0000000..3202b47
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRVerifyAssertionRequest.h
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+#import "FIRAuthRPCRequest.h"
+#import "FIRIdentityToolkitRequest.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @class FIRVerifyAssertionRequest
+ @brief Represents the parameters for the verifyAssertion endpoint.
+ @see https://developers.google.com/identity/toolkit/web/reference/relyingparty/verifyAssertion
+ */
+@interface FIRVerifyAssertionRequest : FIRIdentityToolkitRequest <FIRAuthRPCRequest>
+
+/** @property requestURI
+ @brief The URI to which the IDP redirects the user back. It may contain federated login result
+ params added by the IDP.
+ */
+@property(nonatomic, copy, nullable) NSString *requestURI;
+
+/** @property pendingIDToken
+ @brief The Firebase ID Token for the non-trusted IDP pending to be confirmed by the user.
+ */
+@property(nonatomic, copy, nullable) NSString *pendingIDToken;
+
+/** @property accessToken
+ @brief The STS Access Token for the authenticated user, only needed for linking the user.
+ */
+@property(nonatomic, copy, nullable) NSString *accessToken;
+
+/** @property returnSecureToken
+ @brief Whether the response should return access token and refresh token directly.
+ @remarks The default value is @c YES .
+ */
+@property(nonatomic, assign) BOOL returnSecureToken;
+
+#pragma mark - Components of "postBody"
+
+/** @property providerID
+ @brief The ID of the IDP whose credentials are being presented to the endpoint.
+ */
+@property(nonatomic, copy, readonly) NSString *providerID;
+
+/** @property providerAccessToken
+ @brief An access token from the IDP.
+ */
+@property(nonatomic, copy, nullable) NSString *providerAccessToken;
+
+/** @property providerIDToken
+ @brief An ID Token from the IDP.
+ */
+@property(nonatomic, copy, nullable) NSString *providerIDToken;
+
+/** @property providerOAuthTokenSecret
+ @brief An OAuth client secret from the IDP.
+ */
+@property(nonatomic, copy, nullable) NSString *providerOAuthTokenSecret;
+
+/** @property inputEmail
+ @brief The originally entered email in the UI.
+ */
+@property(nonatomic, copy, nullable) NSString *inputEmail;
+
+/** @property autoCreate
+ @brief A flag that indicates whether or not the user should be automatically created.
+ */
+@property(nonatomic, assign) BOOL autoCreate;
+
+/** @fn initWithEndpoint:APIKey:
+ @brief Please use initWithAPIKey:
+ */
+- (nullable instancetype)initWithEndpoint:(NSString *)endpoint
+ APIKey:(NSString *)APIKey NS_UNAVAILABLE;
+
+/** @fn initWithAPIKey:
+ @brief Designated initializer.
+ @param APIKey The client's API Key.
+ @param providerID The auth provider's ID.
+ */
+- (nullable instancetype)initWithAPIKey:(NSString *)APIKey
+ providerID:(NSString *)providerID NS_DESIGNATED_INITIALIZER;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/RPCs/FIRVerifyAssertionRequest.m b/Firebase/Auth/Source/RPCs/FIRVerifyAssertionRequest.m
new file mode 100644
index 0000000..b31ae42
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRVerifyAssertionRequest.m
@@ -0,0 +1,142 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRVerifyAssertionRequest.h"
+
+#import <GoogleToolboxForMac/GTMNSData+zlib.h>
+#import <GoogleToolboxForMac/GTMNSDictionary+URLArguments.h>
+
+/** @var kVerifyAssertionEndpoint
+ @brief The "verifyAssertion" endpoint.
+ */
+static NSString *const kVerifyAssertionEndpoint = @"verifyAssertion";
+
+/** @var kProviderIDKey
+ @brief The key for the "providerId" value in the request.
+ */
+static NSString *const kProviderIDKey = @"providerId";
+
+/** @var kProviderIDTokenKey
+ @brief The key for the "id_token" value in the request.
+ */
+static NSString *const kProviderIDTokenKey = @"id_token";
+
+/** @var kProviderAccessTokenKey
+ @brief The key for the "access_token" value in the request.
+ */
+static NSString *const kProviderAccessTokenKey = @"access_token";
+
+/** @var kProviderOAuthTokenSecretKey
+ @brief The key for the "oauth_token_secret" value in the request.
+ */
+static NSString *const kProviderOAuthTokenSecretKey = @"oauth_token_secret";
+
+/** @var kIdentifierKey
+ @brief The key for the "identifier" value in the request.
+ */
+static NSString *const kIdentifierKey = @"identifier";
+
+/** @var kRequestURIKey
+ @brief The key for the "requestUri" value in the request.
+ */
+static NSString *const kRequestURIKey = @"requestUri";
+
+/** @var kPostBodyKey
+ @brief The key for the "postBody" value in the request.
+ */
+static NSString *const kPostBodyKey = @"postBody";
+
+/** @var kPendingIDTokenKey
+ @brief The key for the "pendingIdToken" value in the request.
+ */
+static NSString *const kPendingIDTokenKey = @"pendingIdToken";
+
+/** @var kAutoCreateKey
+ @brief The key for the "autoCreate" value in the request.
+ */
+static NSString *const kAutoCreateKey = @"autoCreate";
+
+/** @var kIDTokenKey
+ @brief The key for the "idToken" value in the request. This is actually the STS Access Token,
+ despite it's confusing (backwards compatiable) parameter name.
+ */
+static NSString *const kIDTokenKey = @"idToken";
+
+/** @var kReturnSecureTokenKey
+ @brief The key for the "returnSecureToken" value in the request.
+ */
+static NSString *const kReturnSecureTokenKey = @"returnSecureToken";
+
+@implementation FIRVerifyAssertionRequest
+
+- (nullable instancetype)initWithAPIKey:(NSString *)APIKey
+ providerID:(nonnull NSString *)providerID{
+ self = [super initWithEndpoint:kVerifyAssertionEndpoint APIKey:APIKey];
+ if (self) {
+ _providerID = providerID;
+ _returnSecureToken = YES;
+ _autoCreate = YES;
+ }
+ return self;
+}
+
+- (nullable id)unencodedHTTPRequestBodyWithError:(NSError *_Nullable *_Nullable)error {
+ NSMutableDictionary *postBody = [@{
+ kProviderIDKey : _providerID,
+ } mutableCopy];
+
+ if (_providerIDToken) {
+ postBody[kProviderIDTokenKey] = _providerIDToken;
+ }
+
+ if (_providerAccessToken) {
+ postBody[kProviderAccessTokenKey] = _providerAccessToken;
+ }
+
+ if (!_providerIDToken && !_providerAccessToken) {
+ [NSException raise:NSInvalidArgumentException
+ format:@"Either IDToken or accessToken must be supplied."];
+ }
+
+ if (_providerOAuthTokenSecret) {
+ postBody[kProviderOAuthTokenSecretKey] = _providerOAuthTokenSecret;
+ }
+
+ if (_inputEmail) {
+ postBody[kIdentifierKey] = _inputEmail;
+ }
+
+ NSMutableDictionary *body = [@{
+ kRequestURIKey : @"http://localhost", // Unused by server, but required
+ kPostBodyKey : [postBody gtm_httpArgumentsString]
+ } mutableCopy];
+
+ if (_pendingIDToken) {
+ body[kPendingIDTokenKey] = _pendingIDToken;
+ }
+ if (_accessToken) {
+ body[kIDTokenKey] = _accessToken;
+ }
+ if (_returnSecureToken) {
+ body[kReturnSecureTokenKey] = @YES;
+ }
+
+ body[kAutoCreateKey] = @(_autoCreate);
+
+ return body;
+}
+
+@end
diff --git a/Firebase/Auth/Source/RPCs/FIRVerifyAssertionResponse.h b/Firebase/Auth/Source/RPCs/FIRVerifyAssertionResponse.h
new file mode 100644
index 0000000..ce796ee
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRVerifyAssertionResponse.h
@@ -0,0 +1,186 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+#import "FIRAuthRPCResponse.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @class FIRVerifyAssertionResponse
+ @brief Represents the response from the verifyAssertion endpoint.
+ @see https://developers.google.com/identity/toolkit/web/reference/relyingparty/verifyAssertion
+ */
+@interface FIRVerifyAssertionResponse : NSObject <FIRAuthRPCResponse>
+
+/** @property federatedID
+ @brief The unique ID identifies the IdP account.
+ */
+@property(nonatomic, strong, readonly, nullable) NSString *federatedID;
+
+/** @property providerID
+ @brief The IdP ID. For white listed IdPs it's a short domain name e.g. google.com, aol.com,
+ live.net and yahoo.com. If the "providerId" param is set to OpenID OP identifer other than
+ the whilte listed IdPs the OP identifier is returned. If the "identifier" param is federated
+ ID in the createAuthUri request. The domain part of the federated ID is returned.
+ */
+@property(nonatomic, strong, readonly, nullable) NSString *providerID;
+
+/** @property localID
+ @brief The RP local ID if it's already been mapped to the IdP account identified by the
+ federated ID.
+ */
+@property(nonatomic, strong, readonly, nullable) NSString *localID;
+
+/** @property email
+ @brief The email returned by the IdP. NOTE: The federated login user may not own the email.
+ */
+@property(nonatomic, strong, readonly, nullable) NSString *email;
+
+/** @property inputEmail
+ @brief It's the identifier param in the createAuthUri request if the identifier is an email. It
+ can be used to check whether the user input email is different from the asserted email.
+ */
+@property(nonatomic, strong, readonly, nullable) NSString *inputEmail;
+
+/** @property originalEmail
+ @brief The original email stored in the mapping storage. It's returned when the federated ID is
+ associated to a different email.
+ */
+@property(nonatomic, strong, readonly, nullable) NSString *originalEmail;
+
+/** @property oauthRequestToken
+ @brief The user approved request token for the OpenID OAuth extension.
+ */
+@property(nonatomic, strong, readonly, nullable) NSString *oauthRequestToken;
+
+/** @property oauthScope
+ @brief The scope for the OpenID OAuth extension.
+ */
+@property(nonatomic, strong, readonly, nullable) NSString *oauthScope;
+
+/** @property firstName
+ @brief The first name of the user.
+ */
+@property(nonatomic, strong, readonly, nullable) NSString *firstName;
+
+/** @property lastName
+ @brief The last name of the user.
+ */
+@property(nonatomic, strong, readonly, nullable) NSString *lastName;
+
+/** @property fullName
+ @brief The full name of the user.
+ */
+@property(nonatomic, strong, readonly, nullable) NSString *fullName;
+
+/** @property nickName
+ @brief The nick name of the user.
+ */
+@property(nonatomic, strong, readonly, nullable) NSString *nickName;
+
+/** @property displayName
+ @brief The display name of the user.
+ */
+@property(nonatomic, strong, readonly, nullable) NSString *displayName;
+
+/** @property IDToken
+ @brief Either an authorization code suitable for performing an STS token exchange, or the
+ access token from Secure Token Service, depending on whether @c returnSecureToken is set
+ on the request.
+ */
+@property(nonatomic, strong, readonly, nullable) NSString *IDToken;
+
+/** @property approximateExpirationDate
+ @brief The approximate expiration date of the access token.
+ */
+@property(nonatomic, copy, readonly, nullable) NSDate *approximateExpirationDate;
+
+/** @property refreshToken
+ @brief The refresh token from Secure Token Service.
+ */
+@property(nonatomic, strong, readonly, nullable) NSString *refreshToken;
+
+/** @property action
+ @brief The action code.
+ */
+@property(nonatomic, strong, readonly, nullable) NSString *action;
+
+/** @property language
+ @brief The language preference of the user.
+ */
+@property(nonatomic, strong, readonly, nullable) NSString *language;
+
+/** @property timeZone
+ @brief The timezone of the user.
+ */
+@property(nonatomic, strong, readonly, nullable) NSString *timeZone;
+
+/** @property photoURL
+ @brief The URI of the public accessible profile picture.
+ */
+@property(nonatomic, strong, readonly, nullable) NSURL *photoURL;
+
+/** @property dateOfBirth
+ @brief The birth date of the IdP account.
+ */
+@property(nonatomic, strong, readonly, nullable) NSString *dateOfBirth;
+
+/** @property context
+ @brief The opaque value used by the client to maintain context info between the authentication
+ request and the IDP callback.
+ */
+@property(nonatomic, strong, readonly, nullable) NSString *context;
+
+/** @property verifiedProvider
+ @brief When action is 'map', contains the idps which can be used for confirmation.
+ */
+@property(nonatomic, strong, readonly, nullable) NSArray<NSString *> *verifiedProvider;
+
+/** @property needConfirmation
+ @brief Whether the assertion is from a non-trusted IDP and need account linking confirmation.
+ */
+@property(nonatomic, assign) BOOL needConfirmation;
+
+/** @property emailRecycled
+ @brief It's true if the email is recycled.
+ */
+@property(nonatomic, assign) BOOL emailRecycled;
+
+/** @property emailVerified
+ @brief The value is true if the IDP is also the email provider. It means the user owns the
+ email.
+ */
+@property(nonatomic, assign) BOOL emailVerified;
+
+/** @property isNewUser
+ @brief Flag indicating that the user signing in is a new user and not a returning user.
+ */
+@property(nonatomic, assign) BOOL isNewUser;
+
+/** @property profile
+ @brief Dictionary containing the additional IdP specific information.
+ */
+@property(nonatomic, readonly, nullable) NSDictionary<NSString *, NSObject *> *profile;
+
+/** @property username
+ @brief The name of the user.
+ */
+@property(nonatomic, strong, readonly, nullable) NSString *username;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/RPCs/FIRVerifyAssertionResponse.m b/Firebase/Auth/Source/RPCs/FIRVerifyAssertionResponse.m
new file mode 100644
index 0000000..6ae33c6
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRVerifyAssertionResponse.m
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRVerifyAssertionResponse.h"
+
+#import "../Private/FIRAuthErrorUtils.h"
+
+@implementation FIRVerifyAssertionResponse
+
+- (BOOL)setWithDictionary:(NSDictionary *)dictionary
+ error:(NSError *_Nullable *_Nullable)error {
+ _federatedID = [dictionary[@"federatedId"] copy];
+ _providerID = [dictionary[@"providerId"] copy];
+ _localID = [dictionary[@"localId"] copy];
+ _emailRecycled = [dictionary[@"emailRecycled"] boolValue];
+ _emailVerified = [dictionary[@"emailVerified"] boolValue];
+ _email = [dictionary[@"email"] copy];
+ _inputEmail = [dictionary[@"inputEmail"] copy];
+ _originalEmail = [dictionary[@"originalEmail"] copy];
+ _oauthRequestToken = [dictionary[@"oauthRequestToken"] copy];
+ _oauthScope = [dictionary[@"oauthScope"] copy];
+ _firstName = [dictionary[@"firstName"] copy];
+ _lastName = [dictionary[@"lastName"] copy];
+ _fullName = [dictionary[@"fullName"] copy];
+ _nickName = [dictionary[@"nickName"] copy];
+ _displayName = [dictionary[@"displayName"] copy];
+ _IDToken = [dictionary[@"idToken"] copy];
+ _approximateExpirationDate = [dictionary[@"expiresIn"] isKindOfClass:[NSString class]] ?
+ [NSDate dateWithTimeIntervalSinceNow:[dictionary[@"expiresIn"] doubleValue]] : nil;
+ _refreshToken = [dictionary[@"refreshToken"] copy];
+ _isNewUser = [dictionary[@"isNewUser"] boolValue];
+ id rawUserInfo = dictionary[@"rawUserInfo"];
+ if ([rawUserInfo isKindOfClass:[NSString class]]) {
+ NSData *data = [rawUserInfo dataUsingEncoding:NSUTF8StringEncoding];
+ rawUserInfo = [NSJSONSerialization JSONObjectWithData:data
+ options:NSJSONReadingMutableLeaves
+ error:nil];
+ }
+ if ([rawUserInfo isKindOfClass:[NSDictionary class]]) {
+ _profile = [[NSDictionary alloc] initWithDictionary:rawUserInfo
+ copyItems:YES];
+ }
+ _username = [dictionary[@"username"] copy];
+ _action = [dictionary[@"action"] copy];
+ _language = [dictionary[@"language"] copy];
+ _timeZone = [dictionary[@"timeZone"] copy];
+ _photoURL = dictionary[@"photoUrl"] ? [NSURL URLWithString:dictionary[@"photoUrl"]] : nil;
+ _dateOfBirth = [dictionary[@"dateOfBirth"] copy];
+ _context = [dictionary[@"context"] copy];
+ _needConfirmation = [dictionary[@"needConfirmation"] boolValue];
+ id verifiedProvider = dictionary[@"verifiedProvider"];
+ if ([verifiedProvider isKindOfClass:[NSString class]]) {
+ NSData *data = [verifiedProvider dataUsingEncoding:NSUTF8StringEncoding];
+ verifiedProvider = [NSJSONSerialization JSONObjectWithData:data
+ options:NSJSONReadingMutableLeaves
+ error:nil];
+ }
+ if ([verifiedProvider isKindOfClass:[NSArray class]]) {
+ _verifiedProvider = [[NSArray alloc] initWithArray:verifiedProvider
+ copyItems:YES];
+ }
+ return YES;
+}
+
+@end
diff --git a/Firebase/Auth/Source/RPCs/FIRVerifyClientRequest.h b/Firebase/Auth/Source/RPCs/FIRVerifyClientRequest.h
new file mode 100644
index 0000000..b5da6b8
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRVerifyClientRequest.h
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRIdentityToolkitRequest.h"
+
+#import "FIRAuthRPCRequest.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FIRVerifyClientRequest : FIRIdentityToolkitRequest <FIRAuthRPCRequest>
+
+/** @property appToken
+ @brief The APNS device token.
+ */
+@property(nonatomic, readonly, nullable) NSString *appToken;
+
+/** @property isSandbox
+ @brief The flag that denotes if the appToken pertains to Sandbox or Production.
+ */
+@property(nonatomic, assign, readonly) BOOL isSandbox;
+
+/** @fn initWithEndpoint:APIKey:
+ @brief Please use initWithAppToken:isSandbox: instead.
+ */
+- (nullable instancetype)initWithEndpoint:(NSString *)endpoint
+ APIKey:(NSString *)APIKey NS_UNAVAILABLE;
+
+/** @fn initWithAppToken:isSandbox:
+ @brief Designated initializer.
+ @param appToken The APNS device token.
+ @param isSandbox The flag indicating whether or not the app token provided is for Sandbox or
+ Production.
+ */
+- (nullable instancetype)initWithAppToken:(NSString *)appToken
+ isSandbox:(BOOL)isSandbox
+ APIKey:(NSString *)APIKey NS_DESIGNATED_INITIALIZER;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/RPCs/FIRVerifyClientRequest.m b/Firebase/Auth/Source/RPCs/FIRVerifyClientRequest.m
new file mode 100644
index 0000000..7b4b469
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRVerifyClientRequest.m
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRVerifyClientRequest.h"
+
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @var kVerifyClientEndpoint
+ @brief The endpoint for the verifyClient request.
+ */
+static NSString *const kVerifyClientEndpoint = @"verifyClient";
+
+/** @var kAppTokenKey
+ @brief The key for the appToken request paramenter.
+ */
+static NSString *const kAPPTokenKey = @"appToken";
+
+/** @var kIsSandboxKey
+ @brief The key for the isSandbox request parameter
+ */
+static NSString *const kIsSandboxKey = @"isSandbox";
+
+@implementation FIRVerifyClientRequest
+
+- (nullable instancetype)initWithAppToken:(NSString *)appToken
+ isSandbox:(BOOL)isSandbox
+ APIKey:(NSString *)APIKey {
+ self = [super initWithEndpoint:kVerifyClientEndpoint APIKey:APIKey];
+ if (self) {
+ _appToken = appToken;
+ _isSandbox = isSandbox;
+ }
+ return self;
+}
+
+- (nullable id)unencodedHTTPRequestBodyWithError:(NSError *__autoreleasing _Nullable *)error {
+ NSMutableDictionary *postBody = [NSMutableDictionary dictionary];
+ if (_appToken) {
+ postBody[kAPPTokenKey] = _appToken;
+ }
+ if (_isSandbox) {
+ postBody[kIsSandboxKey] = @YES;
+ }
+ return postBody;
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/RPCs/FIRVerifyClientResponse.h b/Firebase/Auth/Source/RPCs/FIRVerifyClientResponse.h
new file mode 100644
index 0000000..794256a
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRVerifyClientResponse.h
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+#import "FIRAuthRPCResponse.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FIRVerifyClientResponse : NSObject <FIRAuthRPCResponse>
+
+/** @property receipt
+ @brief Receipt that the APNS token was successfully validated with APNS.
+ */
+@property(nonatomic, copy, readonly, nullable) NSString *receipt;
+
+/** @property suggestedTimeOut
+ @brief The date after which delivery of the silent push notification is considered to have
+ failed.
+ */
+@property(nonatomic, copy, readonly, nullable) NSDate *suggestedTimeOutDate;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/RPCs/FIRVerifyClientResponse.m b/Firebase/Auth/Source/RPCs/FIRVerifyClientResponse.m
new file mode 100644
index 0000000..c2477d2
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRVerifyClientResponse.m
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRVerifyClientResponse.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@implementation FIRVerifyClientResponse
+
+- (BOOL)setWithDictionary:(NSDictionary *)dictionary
+ error:(NSError *_Nullable *_Nullable)error {
+ _receipt = dictionary[@"receipt"];
+ _suggestedTimeOutDate = [dictionary[@"suggestedTimeout"] isKindOfClass:[NSString class]] ?
+ [NSDate dateWithTimeIntervalSinceNow:[dictionary[@"suggestedTimeout"] doubleValue]] : nil;
+ return YES;
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/RPCs/FIRVerifyCustomTokenRequest.h b/Firebase/Auth/Source/RPCs/FIRVerifyCustomTokenRequest.h
new file mode 100644
index 0000000..20f3f4d
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRVerifyCustomTokenRequest.h
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+#import "FIRAuthRPCRequest.h"
+#import "FIRIdentityToolkitRequest.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @class FIRVerifyCustomTokenRequest
+ @brief Represents the parameters for the verifyCustomToken endpoint.
+ */
+@interface FIRVerifyCustomTokenRequest : FIRIdentityToolkitRequest <FIRAuthRPCRequest>
+
+/** @property token
+ @brief The self-signed token from the client's BYOAuth server.
+ */
+@property(nonatomic, copy, readonly) NSString *token;
+
+/** @property returnSecureToken
+ @brief Whether the response should return access token and refresh token directly.
+ @remarks The default value is @c YES .
+ */
+@property(nonatomic, assign) BOOL returnSecureToken;
+
+/** @fn initWithEndpoint:APIKey:
+ @brief Please use initWithToken:APIKey:
+ */
+- (nullable instancetype)initWithEndpoint:(NSString *)endpoint
+ APIKey:(NSString *)APIKey NS_UNAVAILABLE;
+
+/** @fn initWithToken:APIKey:
+ @brief Designated initializer.
+ @param token The self-signed token from the client's BYOAuth server.
+ @param APIKey The client's API Key.
+ */
+- (nullable instancetype)initWithToken:(NSString *)token
+ APIKey:(NSString *)APIKey NS_DESIGNATED_INITIALIZER;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/RPCs/FIRVerifyCustomTokenRequest.m b/Firebase/Auth/Source/RPCs/FIRVerifyCustomTokenRequest.m
new file mode 100644
index 0000000..63d72d1
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRVerifyCustomTokenRequest.m
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRVerifyCustomTokenRequest.h"
+
+/** @var kVerifyCustomTokenEndpoint
+ @brief The "verifyPassword" endpoint.
+ */
+static NSString *const kVerifyCustomTokenEndpoint = @"verifyCustomToken";
+
+/** @var kTokenKey
+ @brief The key for the "token" value in the request.
+ */
+static NSString *const kTokenKey = @"token";
+
+/** @var kReturnSecureTokenKey
+ @brief The key for the "returnSecureToken" value in the request.
+ */
+static NSString *const kReturnSecureTokenKey = @"returnSecureToken";
+
+@implementation FIRVerifyCustomTokenRequest
+
+- (nullable instancetype)initWithToken:(NSString *)token
+ APIKey:(NSString *)APIKey {
+ self = [super initWithEndpoint:kVerifyCustomTokenEndpoint APIKey:APIKey];
+ if (self) {
+ _token = [token copy];
+ _returnSecureToken = YES;
+ }
+ return self;
+}
+
+- (nullable id)unencodedHTTPRequestBodyWithError:(NSError *_Nullable *_Nullable)error {
+ NSMutableDictionary *body = [@{
+ kTokenKey : _token
+ } mutableCopy];
+ if (_returnSecureToken) {
+ body[kReturnSecureTokenKey] = @YES;
+ }
+ return body;
+}
+
+@end
diff --git a/Firebase/Auth/Source/RPCs/FIRVerifyCustomTokenResponse.h b/Firebase/Auth/Source/RPCs/FIRVerifyCustomTokenResponse.h
new file mode 100644
index 0000000..b8c215c
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRVerifyCustomTokenResponse.h
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+#import "FIRAuthRPCResponse.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @class FIRVerifyCustomTokenResponse
+ @brief Represents the response from the verifyCustomToken endpoint.
+ */
+@interface FIRVerifyCustomTokenResponse : NSObject <FIRAuthRPCResponse>
+
+/** @property IDToken
+ @brief Either an authorization code suitable for performing an STS token exchange, or the
+ access token from Secure Token Service, depending on whether @c returnSecureToken is set
+ on the request.
+ */
+@property(nonatomic, strong, readonly, nullable) NSString *IDToken;
+
+/** @property approximateExpirationDate
+ @brief The approximate expiration date of the access token.
+ */
+@property(nonatomic, copy, readonly, nullable) NSDate *approximateExpirationDate;
+
+/** @property refreshToken
+ @brief The refresh token from Secure Token Service.
+ */
+@property(nonatomic, strong, readonly, nullable) NSString *refreshToken;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/RPCs/FIRVerifyCustomTokenResponse.m b/Firebase/Auth/Source/RPCs/FIRVerifyCustomTokenResponse.m
new file mode 100644
index 0000000..f86d94b
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRVerifyCustomTokenResponse.m
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRVerifyCustomTokenResponse.h"
+
+#import "../Private/FIRAuthErrorUtils.h"
+
+@implementation FIRVerifyCustomTokenResponse
+
+- (BOOL)setWithDictionary:(NSDictionary *)dictionary
+ error:(NSError *_Nullable *_Nullable)error {
+ _IDToken = [dictionary[@"idToken"] copy];
+ _approximateExpirationDate = [dictionary[@"expiresIn"] isKindOfClass:[NSString class]] ?
+ [NSDate dateWithTimeIntervalSinceNow:[dictionary[@"expiresIn"] doubleValue]] : nil;
+ _refreshToken = [dictionary[@"refreshToken"] copy];
+ return YES;
+}
+
+@end
diff --git a/Firebase/Auth/Source/RPCs/FIRVerifyPasswordRequest.h b/Firebase/Auth/Source/RPCs/FIRVerifyPasswordRequest.h
new file mode 100644
index 0000000..ba54bce
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRVerifyPasswordRequest.h
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+#import "FIRAuthRPCRequest.h"
+#import "FIRIdentityToolkitRequest.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @class FIRVerifyPasswordRequest
+ @brief Represents the parameters for the verifyPassword endpoint.
+ @see https://developers.google.com/identity/toolkit/web/reference/relyingparty/verifyPassword
+ */
+@interface FIRVerifyPasswordRequest : FIRIdentityToolkitRequest <FIRAuthRPCRequest>
+
+/** @property email
+ @brief The email of the user.
+ */
+@property(nonatomic, copy) NSString *email;
+
+/** @property password
+ @brief The password inputed by the user.
+ */
+@property(nonatomic, copy) NSString *password;
+
+/** @property pendingIDToken
+ @brief The GITKit token for the non-trusted IDP, which is to be confirmed by the user.
+ */
+@property(nonatomic, copy, nullable) NSString *pendingIDToken;
+
+/** @property captchaChallenge
+ @brief The captcha challenge.
+ */
+@property(nonatomic, copy, nullable) NSString *captchaChallenge;
+
+/** @property captchaResponse
+ @brief Response to the captcha.
+ */
+@property(nonatomic, copy, nullable) NSString *captchaResponse;
+
+/** @property returnSecureToken
+ @brief Whether the response should return access token and refresh token directly.
+ @remarks The default value is @c YES .
+ */
+@property(nonatomic, assign) BOOL returnSecureToken;
+
+/** @fn initWithEndpoint:APIKey:
+ @brief Please use initWithEmail:password:APIKey:
+ */
+- (nullable instancetype)initWithEndpoint:(NSString *)endpoint
+ APIKey:(NSString *)APIKey NS_UNAVAILABLE;
+
+/** @fn initWithEmail:password:APIKey:
+ @brief Designated initializer.
+ @param email The email of the user.
+ @param password The password inputed by the user.
+ @param APIKey The client's API Key.
+ */
+- (nullable instancetype)initWithEmail:(NSString *)email
+ password:(NSString *)password
+ APIKey:(NSString *)APIKey NS_DESIGNATED_INITIALIZER;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/RPCs/FIRVerifyPasswordRequest.m b/Firebase/Auth/Source/RPCs/FIRVerifyPasswordRequest.m
new file mode 100644
index 0000000..7a9da8b
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRVerifyPasswordRequest.m
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRVerifyPasswordRequest.h"
+
+/** @var kVerifyPasswordEndpoint
+ @brief The "verifyPassword" endpoint.
+ */
+static NSString *const kVerifyPasswordEndpoint = @"verifyPassword";
+
+/** @var kEmailKey
+ @brief The key for the "email" value in the request.
+ */
+static NSString *const kEmailKey = @"email";
+
+/** @var kPasswordKey
+ @brief The key for the "password" value in the request.
+ */
+static NSString *const kPasswordKey = @"password";
+
+/** @var kPendingIDTokenKey
+ @brief The key for the "pendingIdToken" value in the request.
+ */
+static NSString *const kPendingIDTokenKey = @"pendingIdToken";
+
+/** @var kCaptchaChallengeKey
+ @brief The key for the "captchaChallenge" value in the request.
+ */
+static NSString *const kCaptchaChallengeKey = @"captchaChallenge";
+
+/** @var kCaptchaResponseKey
+ @brief The key for the "captchaResponse" value in the request.
+ */
+static NSString *const kCaptchaResponseKey = @"captchaResponse";
+
+/** @var kReturnSecureTokenKey
+ @brief The key for the "returnSecureToken" value in the request.
+ */
+static NSString *const kReturnSecureTokenKey = @"returnSecureToken";
+
+@implementation FIRVerifyPasswordRequest
+
+- (nullable instancetype)initWithEmail:(NSString *)email
+ password:(NSString *)password
+ APIKey:(NSString *)APIKey {
+ self = [super initWithEndpoint:kVerifyPasswordEndpoint APIKey:APIKey];
+ if (self) {
+ _email = [email copy];
+ _password = [password copy];
+ _returnSecureToken = YES;
+ }
+ return self;
+}
+
+- (nullable id)unencodedHTTPRequestBodyWithError:(NSError *_Nullable *_Nullable)error {
+ NSMutableDictionary *postBody = [NSMutableDictionary dictionary];
+ if (_email) {
+ postBody[kEmailKey] = _email;
+ }
+ if (_password) {
+ postBody[kPasswordKey] = _password;
+ }
+ if (_pendingIDToken) {
+ postBody[kPendingIDTokenKey] = _pendingIDToken;
+ }
+ if (_captchaChallenge) {
+ postBody[kCaptchaChallengeKey] = _captchaChallenge;
+ }
+ if (_captchaResponse) {
+ postBody[kCaptchaResponseKey] = _captchaResponse;
+ }
+ if (_returnSecureToken) {
+ postBody[kReturnSecureTokenKey] = @YES;
+ }
+ return postBody;
+}
+
+@end
diff --git a/Firebase/Auth/Source/RPCs/FIRVerifyPasswordResponse.h b/Firebase/Auth/Source/RPCs/FIRVerifyPasswordResponse.h
new file mode 100644
index 0000000..bed13be
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRVerifyPasswordResponse.h
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+#import "FIRAuthRPCResponse.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @class FIRVerifyPasswordResponse
+ @brief Represents the response from the verifyPassword endpoint.
+ @remarks Possible error codes:
+ - FIRAuthInternalErrorCodeUserDisabled
+ - FIRAuthInternalErrorCodeEmailNotFound
+ @see https://developers.google.com/identity/toolkit/web/reference/relyingparty/verifyPassword
+ */
+@interface FIRVerifyPasswordResponse : NSObject <FIRAuthRPCResponse>
+
+/** @property localID
+ @brief The RP local ID if it's already been mapped to the IdP account identified by the
+ federated ID.
+ */
+@property(nonatomic, strong, readonly, nullable) NSString *localID;
+
+/** @property email
+ @brief The email returned by the IdP. NOTE: The federated login user may not own the email.
+ */
+@property(nonatomic, strong, readonly, nullable) NSString *email;
+
+/** @property displayName
+ @brief The display name of the user.
+ */
+@property(nonatomic, strong, readonly, nullable) NSString *displayName;
+
+/** @property IDToken
+ @brief Either an authorization code suitable for performing an STS token exchange, or the
+ access token from Secure Token Service, depending on whether @c returnSecureToken is set
+ on the request.
+ */
+@property(nonatomic, strong, readonly, nullable) NSString *IDToken;
+
+/** @property approximateExpirationDate
+ @brief The approximate expiration date of the access token.
+ */
+@property(nonatomic, copy, readonly, nullable) NSDate *approximateExpirationDate;
+
+/** @property refreshToken
+ @brief The refresh token from Secure Token Service.
+ */
+@property(nonatomic, strong, readonly, nullable) NSString *refreshToken;
+
+/** @property photoURL
+ @brief The URI of the public accessible profile picture.
+ */
+@property(nonatomic, strong, readonly, nullable) NSURL *photoURL;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/RPCs/FIRVerifyPasswordResponse.m b/Firebase/Auth/Source/RPCs/FIRVerifyPasswordResponse.m
new file mode 100644
index 0000000..4d0ca10
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRVerifyPasswordResponse.m
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRVerifyPasswordResponse.h"
+
+#import "../Private/FIRAuthErrorUtils.h"
+
+@implementation FIRVerifyPasswordResponse
+
+- (BOOL)setWithDictionary:(NSDictionary *)dictionary
+ error:(NSError *_Nullable *_Nullable)error {
+ _localID = [dictionary[@"localId"] copy];
+ _email = [dictionary[@"email"] copy];
+ _displayName = [dictionary[@"displayName"] copy];
+ _IDToken = [dictionary[@"idToken"] copy];
+ _approximateExpirationDate = [dictionary[@"expiresIn"] isKindOfClass:[NSString class]] ?
+ [NSDate dateWithTimeIntervalSinceNow:[dictionary[@"expiresIn"] doubleValue]] : nil;
+ _refreshToken = [dictionary[@"refreshToken"] copy];
+ _photoURL = dictionary[@"photoUrl"] ? [NSURL URLWithString:dictionary[@"photoUrl"]] : nil;
+ return YES;
+}
+
+@end
diff --git a/Firebase/Auth/Source/RPCs/FIRVerifyPhoneNumberRequest.h b/Firebase/Auth/Source/RPCs/FIRVerifyPhoneNumberRequest.h
new file mode 100644
index 0000000..06039b9
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRVerifyPhoneNumberRequest.h
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRIdentityToolkitRequest.h"
+
+#import "FIRAuthRPCRequest.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FIRVerifyPhoneNumberRequest : FIRIdentityToolkitRequest <FIRAuthRPCRequest>
+
+/** @property verificationID
+ @brief The verification ID obtained from the response of @c sendVerificationCode.
+*/
+@property(nonatomic, readonly, nullable) NSString *verificationID;
+
+/** @property verificationCode
+ @brief The verification code provided by the user.
+*/
+@property(nonatomic, readonly, nullable) NSString *verificationCode;
+
+/** @property accessToken
+ @brief The STS Access Token for the authenticated user.
+ */
+@property(nonatomic, copy, nullable) NSString *accessToken;
+
+/** @var temporaryProof
+ @brief The a temporary proof code pertaining to this credentil, returned from the backend.
+ */
+@property(nonatomic, readonly, nonnull) NSString *temporaryProof;
+
+/** @var phoneNumber
+ @brief The a phone number pertaining to this credential, returned from the backend.
+ */
+@property(nonatomic, readonly, nonnull) NSString *phoneNumber;
+
+/** @fn initWithEndpoint:APIKey:
+ @brief Please use initWithPhoneNumber:APIKey:
+ */
+- (nullable instancetype)initWithEndpoint:(NSString *)endpoint
+ APIKey:(NSString *)APIKey NS_UNAVAILABLE;
+
+/** @fn initWithTemporaryProof:phoneNumberAPIKey
+ @brief Designated initializer.
+ @param temporaryProof The temporary proof sent by the backed.
+ @param phoneNumber The phone number associated with the credential to be signed in.
+ @param APIKey The client's API Key.
+ */
+- (nullable instancetype)initWithTemporaryProof:(NSString *)temporaryProof
+ phoneNumber:(NSString *)phoneNumber
+ APIKey:(NSString *)APIKey NS_DESIGNATED_INITIALIZER;
+
+/** @fn initWithVerificationID:verificationCode:APIKey
+ @brief Designated initializer.
+ @param verificationID The verification ID obtained from the response of @c sendVerificationCode.
+ @param verificationCode The verification code provided by the user.
+ @param APIKey The client's API Key.
+ */
+- (nullable instancetype)initWithVerificationID:(NSString *)verificationID
+ verificationCode:(NSString *)verificationCode
+ APIKey:(NSString *)APIKey NS_DESIGNATED_INITIALIZER;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/RPCs/FIRVerifyPhoneNumberRequest.m b/Firebase/Auth/Source/RPCs/FIRVerifyPhoneNumberRequest.m
new file mode 100644
index 0000000..b3d1054
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRVerifyPhoneNumberRequest.m
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRVerifyPhoneNumberRequest.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @var kVerifyPhoneNumberEndPoint
+ @brief The "verifyPhoneNumber" endpoint.
+ */
+static NSString *const kVerifyPhoneNumberEndPoint = @"verifyPhoneNumber";
+
+/** @var kVerificationIDKey
+ @brief The key for the verification ID parameter in the request.
+ */
+static NSString *const kVerificationIDKey = @"sessionInfo";
+
+/** @var kVerificationCodeKey
+ @brief The key for the verification code parameter in the request.
+ */
+static NSString *const kVerificationCodeKey = @"code";
+
+/** @var kIDTokenKey
+ @brief The key for the "ID Token" value in the request.
+ */
+static NSString *const kIDTokenKey = @"idToken";
+
+/** @var kTemporaryProofKey
+ @brief The key for the temporary proof value in the request.
+ */
+static NSString *const kTemporaryProofKey = @"temporaryProof";
+
+/** @var kPhoneNumberKey
+ @brief The key for the phone number value in the request.
+ */
+static NSString *const kPhoneNumberKey = @"phoneNumber";
+
+@implementation FIRVerifyPhoneNumberRequest
+
+- (nullable instancetype)initWithTemporaryProof:(NSString *)temporaryProof
+ phoneNumber:(NSString *)phoneNumber
+ APIKey:(NSString *)APIKey {
+ self = [super initWithEndpoint:kVerifyPhoneNumberEndPoint APIKey:APIKey];
+ if (self) {
+ _temporaryProof = [temporaryProof copy];
+ _phoneNumber = [phoneNumber copy];
+ }
+ return self;
+}
+
+- (nullable instancetype)initWithVerificationID:(NSString *)verificationID
+ verificationCode:(NSString *)verificationCode
+ APIKey:(NSString *)APIKey {
+ self = [super initWithEndpoint:kVerifyPhoneNumberEndPoint APIKey:APIKey];
+ if (self) {
+ _verificationID = verificationID;
+ _verificationCode = verificationCode;
+ }
+ return self;
+}
+
+- (nullable id)unencodedHTTPRequestBodyWithError:(NSError *__autoreleasing _Nullable *)error {
+ NSMutableDictionary *postBody = [NSMutableDictionary dictionary];
+ if (_verificationID) {
+ postBody[kVerificationIDKey] = _verificationID;
+ }
+ if (_verificationCode) {
+ postBody[kVerificationCodeKey] = _verificationCode;
+ }
+ if (_accessToken) {
+ postBody[kIDTokenKey] = _accessToken;
+ }
+ if (_temporaryProof) {
+ postBody[kTemporaryProofKey] = _temporaryProof;
+ }
+ if (_phoneNumber) {
+ postBody[kPhoneNumberKey] = _phoneNumber;
+ }
+ return postBody;
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/RPCs/FIRVerifyPhoneNumberResponse.h b/Firebase/Auth/Source/RPCs/FIRVerifyPhoneNumberResponse.h
new file mode 100644
index 0000000..f8f4ba2
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRVerifyPhoneNumberResponse.h
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+#import "FIRAuthRPCResponse.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FIRVerifyPhoneNumberResponse : NSObject <FIRAuthRPCResponse>
+
+/** @property IDToken
+ @brief Either an authorization code suitable for performing an STS token exchange, or the
+ access token from Secure Token Service, depending on whether @c returnSecureToken is set
+ on the request.
+ */
+@property(nonatomic, strong, readonly, nullable) NSString *IDToken;
+
+/** @property refreshToken
+ @brief The refresh token from Secure Token Service.
+ */
+@property(nonatomic, strong, readonly, nullable) NSString *refreshToken;
+
+/** @property localID
+ @brief The Firebear user ID.
+ */
+@property(nonatomic, strong, readonly, nullable) NSString *localID;
+
+/** @property phoneNumber
+ @brief The verified phone number.
+ */
+@property(nonatomic, strong, readonly, nullable) NSString *phoneNumber;
+
+/** @property temporaryProof
+ @brief The temporary proof code returned by the backend.
+ */
+@property(nonatomic, strong, readonly, nullable) NSString *temporaryProof;
+
+/** @property isNewUser
+ @brief Flag indicating that the user signing in is a new user and not a returning user.
+ */
+@property(nonatomic, assign) BOOL isNewUser;
+
+/** @property approximateExpirationDate
+ @brief The approximate expiration date of the access token.
+ */
+@property(nonatomic, copy, readonly, nullable) NSDate *approximateExpirationDate;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/RPCs/FIRVerifyPhoneNumberResponse.m b/Firebase/Auth/Source/RPCs/FIRVerifyPhoneNumberResponse.m
new file mode 100644
index 0000000..acba2c2
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRVerifyPhoneNumberResponse.m
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRVerifyPhoneNumberResponse.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@implementation FIRVerifyPhoneNumberResponse
+
+- (nullable NSString *)expectedKind {
+ return nil;
+}
+
+- (BOOL)setWithDictionary:(NSDictionary *)dictionary
+ error:(NSError *_Nullable *_Nullable)error {
+ _IDToken = [dictionary[@"idToken"] copy];
+ _refreshToken = [dictionary[@"refreshToken"] copy];
+ _isNewUser = [dictionary[@"isNewUser"] boolValue];
+ _localID = [dictionary[@"localId"] copy];
+ _phoneNumber = [dictionary[@"phoneNumber"] copy];
+ _temporaryProof = [dictionary[@"temporaryProof"] copy];
+ _approximateExpirationDate = [dictionary[@"expiresIn"] isKindOfClass:[NSString class]] ?
+ [NSDate dateWithTimeIntervalSinceNow:[dictionary[@"expiresIn"] doubleValue]] : nil;
+ return YES;
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Core/FIRAnalyticsConfiguration.h b/Firebase/Core/FIRAnalyticsConfiguration.h
new file mode 100644
index 0000000..f42eaf5
--- /dev/null
+++ b/Firebase/Core/FIRAnalyticsConfiguration.h
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+#import "FIRCoreSwiftNameSupport.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * This class provides configuration fields for Firebase Analytics.
+ */
+FIR_SWIFT_NAME(AnalyticsConfiguration)
+@interface FIRAnalyticsConfiguration : NSObject
+
+/**
+ * Returns the shared instance of FIRAnalyticsConfiguration.
+ */
++ (FIRAnalyticsConfiguration *)sharedInstance FIR_SWIFT_NAME(shared());
+
+/**
+ * Sets the minimum engagement time in seconds required to start a new session. The default value
+ * is 10 seconds.
+ */
+- (void)setMinimumSessionInterval:(NSTimeInterval)minimumSessionInterval;
+
+/**
+ * Sets the interval of inactivity in seconds that terminates the current session. The default
+ * value is 1800 seconds (30 minutes).
+ */
+- (void)setSessionTimeoutInterval:(NSTimeInterval)sessionTimeoutInterval;
+
+/**
+ * Sets whether analytics collection is enabled for this app on this device. This setting is
+ * persisted across app sessions. By default it is enabled.
+ */
+- (void)setAnalyticsCollectionEnabled:(BOOL)analyticsCollectionEnabled;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Core/FIRAnalyticsConfiguration.m b/Firebase/Core/FIRAnalyticsConfiguration.m
new file mode 100644
index 0000000..cec3771
--- /dev/null
+++ b/Firebase/Core/FIRAnalyticsConfiguration.m
@@ -0,0 +1,61 @@
+// Copyright 2017 Google
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "FIRAnalyticsConfiguration.h"
+
+#import "Private/FIRAnalyticsConfiguration+Internal.h"
+
+@implementation FIRAnalyticsConfiguration
+
++ (FIRAnalyticsConfiguration *)sharedInstance {
+ static FIRAnalyticsConfiguration *sharedInstance = nil;
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ sharedInstance = [[FIRAnalyticsConfiguration alloc] init];
+ });
+ return sharedInstance;
+}
+
+- (void)postNotificationName:(NSString *)name value:(id)value {
+ if (!name.length || !value) {
+ return;
+ }
+ [[NSNotificationCenter defaultCenter] postNotificationName:name
+ object:self
+ userInfo:@{ name : value }];
+}
+
+- (void)setMinimumSessionInterval:(NSTimeInterval)minimumSessionInterval {
+ [self postNotificationName:kFIRAnalyticsConfigurationSetMinimumSessionIntervalNotification
+ value:@(minimumSessionInterval)];
+}
+
+- (void)setSessionTimeoutInterval:(NSTimeInterval)sessionTimeoutInterval {
+ [self postNotificationName:kFIRAnalyticsConfigurationSetSessionTimeoutIntervalNotification
+ value:@(sessionTimeoutInterval)];
+}
+
+- (void)setAnalyticsCollectionEnabled:(BOOL)analyticsCollectionEnabled {
+ // Persist the measurementEnabledState. Use FIRAnalyticsEnabledState values instead of YES/NO.
+ FIRAnalyticsEnabledState analyticsEnabledState =
+ analyticsCollectionEnabled ? kFIRAnalyticsEnabledStateSetYes : kFIRAnalyticsEnabledStateSetNo;
+ [[NSUserDefaults standardUserDefaults] setObject:@(analyticsEnabledState)
+ forKey:kFIRAPersistedConfigMeasurementEnabledStateKey];
+ [[NSUserDefaults standardUserDefaults] synchronize];
+
+ [self postNotificationName:kFIRAnalyticsConfigurationSetEnabledNotification
+ value:@(analyticsCollectionEnabled)];
+}
+
+@end
diff --git a/Firebase/Core/FIRApp.h b/Firebase/Core/FIRApp.h
new file mode 100644
index 0000000..7f1d0c7
--- /dev/null
+++ b/Firebase/Core/FIRApp.h
@@ -0,0 +1,126 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+#import <UIKit/UIKit.h>
+
+#import "FIRCoreSwiftNameSupport.h"
+
+@class FIROptions;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** A block that takes a BOOL and has no return value. */
+typedef void (^FIRAppVoidBoolCallback)(BOOL success) FIR_SWIFT_NAME(FirebaseAppVoidBoolCallback);
+
+/**
+ * The entry point of Firebase SDKs.
+ *
+ * Initialize and configure FIRApp using +[FIRApp configure]
+ * or other customized ways as shown below.
+ *
+ * The logging system has two modes: default mode and debug mode. In default mode, only logs with
+ * log level Notice, Warning and Error will be sent to device. In debug mode, all logs will be sent
+ * to device. The log levels that Firebase uses are consistent with the ASL log levels.
+ *
+ * Enable debug mode by passing the -FIRDebugEnabled argument to the application. You can add this
+ * argument in the application's Xcode scheme. When debug mode is enabled via -FIRDebugEnabled,
+ * further executions of the application will also be in debug mode. In order to return to default
+ * mode, you must explicitly disable the debug mode with the application argument -FIRDebugDisabled.
+ *
+ * It is also possible to change the default logging level in code by calling setLoggerLevel: on
+ * the FIRConfiguration interface.
+ */
+FIR_SWIFT_NAME(FirebaseApp)
+@interface FIRApp : NSObject
+
+/**
+ * Configures a default Firebase app. Raises an exception if any configuration step fails. The
+ * default app is named "__FIRAPP_DEFAULT". This method should be called after the app is launched
+ * and before using Firebase services. This method is thread safe.
+ */
++ (void)configure;
+
+/**
+ * Configures the default Firebase app with the provided options. The default app is named
+ * "__FIRAPP_DEFAULT". Raises an exception if any configuration step fails. This method is thread
+ * safe.
+ *
+ * @param options The Firebase application options used to configure the service.
+ */
++ (void)configureWithOptions:(FIROptions *)options FIR_SWIFT_NAME(configure(options:));
+
+/**
+ * Configures a Firebase app with the given name and options. Raises an exception if any
+ * configuration step fails. This method is thread safe.
+ *
+ * @param name The application's name given by the developer. The name should should only contain
+ Letters, Numbers and Underscore.
+ * @param options The Firebase application options used to configure the services.
+ */
++ (void)configureWithName:(NSString *)name options:(FIROptions *)options
+ FIR_SWIFT_NAME(configure(name:options:));
+
+/**
+ * Returns the default app, or nil if the default app does not exist.
+ */
++ (nullable FIRApp *)defaultApp FIR_SWIFT_NAME(app());
+
+/**
+ * Returns a previously created FIRApp instance with the given name, or nil if no such app exists.
+ * This method is thread safe.
+ */
++ (nullable FIRApp *)appNamed:(NSString *)name FIR_SWIFT_NAME(app(name:));
+
+#if defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0
+/**
+ * Returns the set of all extant FIRApp instances, or nil if there are no FIRApp instances. This
+ * method is thread safe.
+ */
+@property(class, readonly, nullable) NSDictionary <NSString *, FIRApp *> *allApps;
+#else
+/**
+ * Returns the set of all extant FIRApp instances, or nil if there are no FIRApp instances. This
+ * method is thread safe.
+ */
++ (nullable NSDictionary <NSString *, FIRApp *> *)allApps FIR_SWIFT_NAME(allApps());
+#endif // defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0
+
+/**
+ * Cleans up the current FIRApp, freeing associated data and returning its name to the pool for
+ * future use. This method is thread safe.
+ */
+- (void)deleteApp:(FIRAppVoidBoolCallback)completion;
+
+/**
+ * FIRApp instances should not be initialized directly. Call +[FIRApp configure],
+ * +[FIRApp configureWithOptions:], or +[FIRApp configureWithNames:options:] directly.
+ */
+- (instancetype)init NS_UNAVAILABLE;
+
+/**
+ * Gets the name of this app.
+ */
+@property(nonatomic, copy, readonly) NSString *name;
+
+/**
+ * Gets a copy of the options for this app. These are non-modifiable.
+ */
+@property(nonatomic, copy, readonly) FIROptions *options;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Core/FIRApp.m b/Firebase/Core/FIRApp.m
new file mode 100644
index 0000000..86784a3
--- /dev/null
+++ b/Firebase/Core/FIRApp.m
@@ -0,0 +1,596 @@
+// Copyright 2017 Google
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include <sys/utsname.h>
+
+#import "FIRApp.h"
+#import "Private/FIRAppInternal.h"
+#import "Private/FIRBundleUtil.h"
+#import "FIRConfiguration.h"
+#import "Private/FIRLogger.h"
+#import "Private/FIROptionsInternal.h"
+
+NSString *const kFIRServiceAdMob = @"AdMob";
+NSString *const kFIRServiceAuth = @"Auth";
+NSString *const kFIRServiceCrash = @"Crash";
+NSString *const kFIRServiceDatabase = @"Database";
+NSString *const kFIRServiceDynamicLinks = @"DynamicLinks";
+NSString *const kFIRServiceInstanceID = @"InstanceID";
+NSString *const kFIRServiceInvites = @"Invites";
+NSString *const kFIRServiceMessaging = @"Messaging";
+NSString *const kFIRServiceMeasurement = @"Measurement";
+NSString *const kFIRServiceRemoteConfig = @"RemoteConfig";
+NSString *const kFIRServiceStorage = @"Storage";
+NSString *const kGGLServiceAnalytics = @"Analytics";
+NSString *const kGGLServiceSignIn = @"SignIn";
+
+NSString *const kFIRDefaultAppName = @"__FIRAPP_DEFAULT";
+NSString *const kFIRAppReadyToConfigureSDKNotification = @"FIRAppReadyToConfigureSDKNotification";
+NSString *const kFIRAppDeleteNotification = @"FIRAppDeleteNotification";
+NSString *const kFIRAppIsDefaultAppKey = @"FIRAppIsDefaultAppKey";
+NSString *const kFIRAppNameKey = @"FIRAppNameKey";
+NSString *const kFIRGoogleAppIDKey = @"FIRGoogleAppIDKey";
+
+NSString *const kFIRAppDiagnosticsNotification = @"FIRAppDiagnosticsNotification";
+
+NSString *const kFIRAppDiagnosticsConfigurationTypeKey = @"ConfigType";
+NSString *const kFIRAppDiagnosticsErrorKey = @"Error";
+NSString *const kFIRAppDiagnosticsFIRAppKey = @"FIRApp";
+NSString *const kFIRAppDiagnosticsSDKNameKey = @"SDKName";
+NSString *const kFIRAppDiagnosticsSDKVersionKey = @"SDKVersion";
+
+/**
+ * The URL to download plist files.
+ */
+static NSString *const kPlistURL = @"https://console.firebase.google.com/";
+
+@interface FIRApp ()
+
+@property(nonatomic) BOOL alreadySentConfigureNotification;
+
+@property(nonatomic) BOOL alreadySentDeleteNotification;
+
+@end
+
+@implementation FIRApp
+
+// This is necessary since our custom getter prevents `_options` from being created.
+@synthesize options = _options;
+
+static NSMutableDictionary *sAllApps;
+static FIRApp *sDefaultApp;
+
++ (void)configure {
+ FIROptions *options = [FIROptions defaultOptions];
+ if (!options) {
+ [[NSNotificationCenter defaultCenter]
+ postNotificationName:kFIRAppDiagnosticsNotification
+ object:nil
+ userInfo:@{
+ kFIRAppDiagnosticsConfigurationTypeKey : @(FIRConfigTypeCore),
+ kFIRAppDiagnosticsErrorKey : [FIRApp errorForMissingOptions]
+ }];
+ [NSException raise:kFirebaseCoreErrorDomain
+ format:@"[FIRApp configure] could not find a valid GoogleService-Info.plist in "
+ @"your project. Please download one from %@.",
+ kPlistURL];
+ }
+ [FIRApp configureDefaultAppWithOptions:options sendingNotifications:YES];
+}
+
++ (void)configureWithOptions:(FIROptions *)options {
+ if (!options) {
+ [NSException raise:kFirebaseCoreErrorDomain
+ format:@"Options is nil. Please pass a valid options."];
+ }
+ [FIRApp configureDefaultAppWithOptions:options sendingNotifications:YES];
+}
+
++ (void)configureWithoutSendingNotification {
+ FIROptions *options = [FIROptions defaultOptions];
+ if (!options) {
+ [[NSNotificationCenter defaultCenter]
+ postNotificationName:kFIRAppDiagnosticsNotification
+ object:nil
+ userInfo:@{
+ kFIRAppDiagnosticsConfigurationTypeKey : @(FIRConfigTypeCore),
+ kFIRAppDiagnosticsErrorKey : [FIRApp errorForMissingOptions]
+ }];
+ [NSException raise:kFirebaseCoreErrorDomain format:@"Please check there is a valid "
+ @"GoogleService-Info.plist in the project."];
+ }
+ [FIRApp configureDefaultAppWithOptions:options sendingNotifications:NO];
+}
+
++ (void)configureDefaultAppWithOptions:(FIROptions *)options
+ sendingNotifications:(BOOL)sendNotifications {
+ if (sDefaultApp) {
+ // FIRApp sets up FirebaseAnalytics and does plist validation, but does not cause it
+ // to fire notifications. So, if the default app already exists, but has not sent out
+ // configuration notifications, then continue re-initializing it.
+ if (!sendNotifications || sDefaultApp.alreadySentConfigureNotification) {
+ [NSException raise:kFirebaseCoreErrorDomain
+ format:@"Default app has already been configured."];
+ }
+ }
+ @synchronized(self) {
+ FIRLogDebug(kFIRLoggerCore, @"I-COR000001", @"Configuring the default app.");
+ sDefaultApp = [[FIRApp alloc] initInstanceWithName:kFIRDefaultAppName options:options];
+ [FIRApp addAppToAppDictionary:sDefaultApp];
+ if (!sDefaultApp.alreadySentConfigureNotification && sendNotifications) {
+ [FIRApp sendNotificationsToSDKs:sDefaultApp];
+ sDefaultApp.alreadySentConfigureNotification = YES;
+ }
+ }
+}
+
++ (void)configureWithName:(NSString *)name options:(FIROptions *)options {
+ if (!name || !options) {
+ [NSException raise:kFirebaseCoreErrorDomain format:@"Neither name nor options can be nil."];
+ }
+ if (name.length == 0) {
+ [NSException raise:kFirebaseCoreErrorDomain format:@"Name cannot be empty."];
+ }
+ if ([name isEqualToString:kFIRDefaultAppName]) {
+ [NSException raise:kFirebaseCoreErrorDomain format:@"Name cannot be __FIRAPP_DEFAULT."];
+ }
+ NSString *lowerCaseName = [name lowercaseString];
+ for (NSInteger charIndex = 0; charIndex < lowerCaseName.length; charIndex++) {
+ char character = [lowerCaseName characterAtIndex:charIndex];
+ if (!((character >= 'a' && character <= 'z')
+ || (character >= '0' && character <= '9')
+ || character == '_'
+ || character == '-')) {
+ [NSException raise:kFirebaseCoreErrorDomain format:@"App name should only contain Letters, "
+ @"Numbers, Underscores, and Dashes."];
+ }
+ }
+
+ if (sAllApps && sAllApps[name]) {
+ [NSException raise:kFirebaseCoreErrorDomain
+ format:@"App named %@ has already been configured.", name];
+ }
+
+ @synchronized(self) {
+ FIRLogDebug(kFIRLoggerCore, @"I-COR000002", @"Configuring app named %@", name);
+ FIRApp *app = [[FIRApp alloc] initInstanceWithName:name options:options];
+ [FIRApp addAppToAppDictionary:app];
+ if (!app.alreadySentConfigureNotification) {
+ [FIRApp sendNotificationsToSDKs:app];
+ app.alreadySentConfigureNotification = YES;
+ }
+ }
+}
+
++ (FIRApp *)defaultApp {
+ if (sDefaultApp) {
+ return sDefaultApp;
+ }
+ FIRLogError(kFIRLoggerCore, @"I-COR000003", @"The default Firebase app has not yet been "
+ @"configured. Add [FIRApp configure] to your application initialization. Read more: "
+ @"https://goo.gl/ctyzm8.");
+ return nil;
+}
+
++ (FIRApp *)appNamed:(NSString *)name {
+ @synchronized(self) {
+ if (sAllApps) {
+ FIRApp *app = sAllApps[name];
+ if (app) {
+ return app;
+ }
+ }
+ FIRLogError(kFIRLoggerCore, @"I-COR000004", @"App with name %@ does not exist.", name);
+ return nil;
+ }
+}
+
++ (NSDictionary *)allApps {
+ @synchronized(self) {
+ if (!sAllApps) {
+ FIRLogError(kFIRLoggerCore, @"I-COR000005", @"No app has been configured yet.");
+ }
+ NSDictionary *dict = [NSDictionary dictionaryWithDictionary:sAllApps];
+ return dict;
+ }
+}
+
+// Public only for tests
++ (void)resetApps {
+ sDefaultApp = nil;
+ [sAllApps removeAllObjects];
+ sAllApps = nil;
+}
+
+- (void)deleteApp:(FIRAppVoidBoolCallback)completion {
+ @synchronized([self class]) {
+ if (sAllApps && sAllApps[self.name]) {
+ FIRLogDebug(kFIRLoggerCore, @"I-COR000006", @"Deleting app named %@", self.name);
+ [sAllApps removeObjectForKey:self.name];
+ if ([self.name isEqualToString:kFIRDefaultAppName]) {
+ sDefaultApp = nil;
+ }
+ if (!self.alreadySentDeleteNotification) {
+ NSDictionary *appInfoDict = @ {
+ kFIRAppNameKey : self.name
+ };
+ [[NSNotificationCenter defaultCenter] postNotificationName:kFIRAppDeleteNotification
+ object:[self class]
+ userInfo:appInfoDict];
+ self.alreadySentDeleteNotification = YES;
+ }
+ completion(YES);
+ } else {
+ FIRLogError(kFIRLoggerCore, @"I-COR000007", @"App does not exist.");
+ completion(NO);
+ }
+ }
+}
+
++ (void)addAppToAppDictionary:(FIRApp *)app {
+ if (!sAllApps) {
+ sAllApps = [NSMutableDictionary dictionary];
+ }
+ if ([app configureCore]) {
+ sAllApps[app.name] = app;
+ [[NSNotificationCenter defaultCenter]
+ postNotificationName:kFIRAppDiagnosticsNotification
+ object:nil
+ userInfo:@{
+ kFIRAppDiagnosticsConfigurationTypeKey : @(FIRConfigTypeCore),
+ kFIRAppDiagnosticsFIRAppKey : app
+ }];
+ } else {
+ [NSException raise:kFirebaseCoreErrorDomain
+ format:@"Configuration fails. It may be caused by an invalid GOOGLE_APP_ID in "
+ @"GoogleService-Info.plist or set in the customized options."];
+ }
+}
+
+- (instancetype)initInstanceWithName:(NSString *)name options:(FIROptions *)options {
+ self = [super init];
+ if (self) {
+ _name = [name copy];
+ _options = [options copy];
+ _options.editingLocked = YES;
+
+ FIRApp *app = sAllApps[name];
+ _alreadySentConfigureNotification = app.alreadySentConfigureNotification;
+ _alreadySentDeleteNotification = app.alreadySentDeleteNotification;
+ }
+ return self;
+}
+
+- (void)getTokenForcingRefresh:(BOOL)forceRefresh withCallback:(FIRTokenCallback)callback {
+ if (!_getTokenImplementation) {
+ callback(nil, nil);
+ return;
+ }
+
+ _getTokenImplementation(forceRefresh, callback);
+}
+
+- (BOOL)configureCore {
+ [self checkExpectedBundleID];
+ if (![self isAppIDValid]) {
+ if (_options.usingOptionsFromDefaultPlist) {
+ [[NSNotificationCenter defaultCenter]
+ postNotificationName:kFIRAppDiagnosticsNotification
+ object:nil
+ userInfo:@{
+ kFIRAppDiagnosticsConfigurationTypeKey : @(FIRConfigTypeCore),
+ kFIRAppDiagnosticsErrorKey : [FIRApp errorForInvalidAppID],
+ }];
+ }
+ return NO;
+ }
+
+ if (NSClassFromString(@"FIRAppIndexing") != nil) {
+ FIRLogDebug(kFIRLoggerCore, @"I-COR000024", @"Firebase App Indexing on iOS is deprecated. "
+ @"You don't need to take any action at this time. Learn more about Firebase App "
+ @"Indexing at https://firebase.google.com/docs/app-indexing/.");
+ }
+
+ // Initialize the Analytics once there is a valid options under default app. Analytics should
+ // always initialize first by itself before the other SDKs.
+ if ([self.name isEqualToString:kFIRDefaultAppName]) {
+ Class firAnalyticsClass = NSClassFromString(@"FIRAnalytics");
+ if (!firAnalyticsClass) {
+ FIRLogError(kFIRLoggerCore, @"I-COR000022", @"Firebase Analytics is not available.");
+ } else {
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wundeclared-selector"
+ SEL startWithConfigurationSelector = @selector(startWithConfiguration:options:);
+#pragma clang diagnostic pop
+ if ([firAnalyticsClass respondsToSelector:startWithConfigurationSelector]) {
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
+ [firAnalyticsClass performSelector:startWithConfigurationSelector
+ withObject:[FIRConfiguration sharedInstance].analyticsConfiguration
+ withObject:_options];
+#pragma clang diagnostic pop
+ }
+ }
+ }
+ return YES;
+}
+
+- (FIROptions *)options {
+ return [_options copy];
+}
+
+#pragma mark - private
+
++ (void)sendNotificationsToSDKs:(FIRApp *)app {
+ NSNumber *isDefaultApp = [NSNumber numberWithBool:(app == sDefaultApp)];
+ NSDictionary *appInfoDict = @ {
+ kFIRAppNameKey : app.name,
+ kFIRAppIsDefaultAppKey : isDefaultApp,
+ kFIRGoogleAppIDKey : app.options.googleAppID
+ };
+ [[NSNotificationCenter defaultCenter] postNotificationName:kFIRAppReadyToConfigureSDKNotification
+ object:self
+ userInfo:appInfoDict];
+}
+
++ (NSError *)errorForMissingOptions {
+ NSDictionary *errorDict = @{
+ NSLocalizedDescriptionKey :
+ @"Unable to parse GoogleService-Info.plist in order to configure services.",
+ NSLocalizedRecoverySuggestionErrorKey :
+ @"Check formatting and location of GoogleService-Info.plist."
+ };
+ return FIRCreateError(kFirebaseCoreErrorDomain, FIRErrorCodeInvalidPlistFile, errorDict);
+}
+
++ (NSError *)errorForSubspecConfigurationFailureWithDomain:(NSString *)domain
+ errorCode:(FIRErrorCode)code
+ service:(NSString *)service
+ reason:(NSString *)reason {
+ NSString *description =
+ [NSString stringWithFormat:@"Configuration failed for service %@.", service];
+ NSDictionary *errorDict = @{
+ NSLocalizedDescriptionKey : description,
+ NSLocalizedFailureReasonErrorKey : reason
+ };
+ return FIRCreateError(domain, code, errorDict);
+}
+
++ (NSError *)errorForInvalidAppID {
+ NSDictionary *errorDict = @{
+ NSLocalizedDescriptionKey :
+ @"Unable to validate Google App ID",
+ NSLocalizedRecoverySuggestionErrorKey :
+ @"Check formatting and location of GoogleService-Info.plist or GoogleAppID set in the "
+ @"customized options."
+ };
+ return FIRCreateError(kFirebaseCoreErrorDomain, FIRErrorCodeInvalidAppID, errorDict);
+}
+
+- (void)checkExpectedBundleID {
+ NSArray *bundles = [FIRBundleUtil relevantBundles];
+ NSString *expectedBundleID = [self expectedBundleID];
+ // The checking is only done when the bundle ID is provided in the serviceInfo dictionary for
+ // backward compatibility.
+ if (expectedBundleID != nil &&
+ ![FIRBundleUtil hasBundleIdentifier:expectedBundleID inBundles:bundles]) {
+ FIRLogInfo(kFIRLoggerCore, @"I-COR000008", @"The project's Bundle ID is inconsistent with "
+ @"either the Bundle ID in '%@.%@', or the Bundle ID in the options if you are "
+ @"using a customized options. To ensure that everything can be configured "
+ @"correctly, you may need to make the Bundle IDs consistent. To continue with this "
+ @"plist file, you may change your app's bundle identifier to '%@'. Or you can "
+ @"download a new configuration file that matches your bundle identifier from %@ "
+ @"and replace the current one.", kServiceInfoFileName, kServiceInfoFileType,
+ expectedBundleID, kPlistURL);
+ }
+}
+
+- (nullable NSString *)getUID {
+ if (!_getUIDImplementation) {
+ FIRLogWarning(kFIRLoggerCore, @"I-COR000025", @"FIRAuth getUID implementation wasn't set.");
+ return nil;
+ }
+ return _getUIDImplementation();
+}
+
+#pragma mark - private - App ID Validation
+
+/**
+ * Validates the format and fingerprint of the app ID contained in GOOGLE_APP_ID in the plist file.
+ * This is the main method for validating app ID.
+ *
+ * @return YES if the app ID fulfills the expected format and fingerprint, NO otherwise.
+ */
+- (BOOL)isAppIDValid {
+ NSString *appID = _options.googleAppID;
+ BOOL isValid = [FIRApp validateAppID:appID];
+ if (!isValid){
+ NSString *expectedBundleID = [self expectedBundleID];
+ FIRLogError(kFIRLoggerCore, @"I-COR000009", @"The GOOGLE_APP_ID either in the plist file "
+ @"'%@.%@' or the one set in the customized options is invalid. If you are using "
+ @"the plist file, use the iOS version of bundle identifier to download the file, "
+ @"and do not manually edit the GOOGLE_APP_ID. You may change your app's bundle "
+ @"identifier to '%@'. Or you can download a new configuration file that matches "
+ @"your bundle identifier from %@ and replace the current one.",
+ kServiceInfoFileName, kServiceInfoFileType, expectedBundleID, kPlistURL);
+
+ };
+ return isValid;
+}
+
++ (BOOL)validateAppID:(NSString *)appID {
+ // Failing validation only occurs when we are sure we are looking at a V2 app ID and it does not
+ // have a valid fingerprint, otherwise we just warn about the potential issue.
+ if (!appID.length) {
+ return NO;
+ }
+
+ // All app IDs must start with at least "<version number>:".
+ NSString *const versionPattern = @"^\\d+:";
+ NSRegularExpression *versionRegex =
+ [NSRegularExpression regularExpressionWithPattern:versionPattern options:0 error:NULL];
+ if (!versionRegex) {
+ return NO;
+ }
+
+ NSRange appIDRange = NSMakeRange(0, appID.length);
+ NSArray *versionMatches = [versionRegex matchesInString:appID options:0 range:appIDRange];
+ if (versionMatches.count != 1) {
+ return NO;
+ }
+
+ NSRange versionRange = [(NSTextCheckingResult *)versionMatches.firstObject range];
+ NSString *appIDVersion = [appID substringWithRange:versionRange];
+ NSArray *knownVersions = @[ @"1:" ];
+ if (![knownVersions containsObject:appIDVersion]) {
+ // Permit unknown yet properly formatted app ID versions.
+ return YES;
+ }
+
+ if (![FIRApp validateAppIDFormat:appID withVersion:appIDVersion]) {
+ return NO;
+ }
+
+ if (![FIRApp validateAppIDFingerprint:appID withVersion:appIDVersion]) {
+ return NO;
+ }
+
+ return YES;
+}
+
++ (NSString *)actualBundleID {
+ return [[NSBundle mainBundle] bundleIdentifier];
+}
+
+/**
+ * Validates that the format of the app ID string is what is expected based on the supplied version.
+ * The version must end in ":".
+ *
+ * For v1 app ids the format is expected to be
+ * '<version #>:<project number>:ios:<fingerprint of bundle id>'.
+ *
+ * This method does not verify that the contents of the app id are correct, just that they fulfill
+ * the expected format.
+ *
+ * @param appID Contents of GOOGLE_APP_ID from the plist file.
+ * @param version Indicates what version of the app id format this string should be.
+ * @return YES if provided string fufills the expected format, NO otherwise.
+ */
++ (BOOL)validateAppIDFormat:(NSString *)appID withVersion:(NSString *)version {
+ if (!appID.length || !version.length) {
+ return NO;
+ }
+
+ if (![version hasSuffix:@":"]) {
+ return NO;
+ }
+
+ if (![appID hasPrefix:version]) {
+ return NO;
+ }
+
+ NSString *const pattern = @"^\\d+:ios:[a-f0-9]+$";
+ NSRegularExpression *regex =
+ [NSRegularExpression regularExpressionWithPattern:pattern options:0 error:NULL];
+ if (!regex) {
+ return NO;
+ }
+
+ NSRange localRange = NSMakeRange(version.length, appID.length - version.length);
+ NSUInteger numberOfMatches = [regex numberOfMatchesInString:appID options:0 range:localRange];
+ if (numberOfMatches != 1) {
+ return NO;
+ }
+ return YES;
+}
+
+/**
+ * Validates that the fingerprint of the app ID string is what is expected based on the supplied
+ * version. The version must end in ":".
+ *
+ * Note that the v1 hash algorithm is not permitted on the client and cannot be fully validated.
+ *
+ * @param appID Contents of GOOGLE_APP_ID from the plist file.
+ * @param version Indicates what version of the app id format this string should be.
+ * @return YES if provided string fufills the expected fingerprint and the version is known, NO
+ * otherwise.
+ */
++ (BOOL)validateAppIDFingerprint:(NSString *)appID withVersion:(NSString *)version {
+ if (!appID.length || !version.length) {
+ return NO;
+ }
+
+ if (![version hasSuffix:@":"]) {
+ return NO;
+ }
+
+ if (![appID hasPrefix:version]) {
+ return NO;
+ }
+
+ // Extract the supplied fingerprint from the supplied app ID.
+ // This assumes the app ID format is the same for all known versions below. If the app ID format
+ // changes in future versions, the tokenizing of the app ID format will need to take into account
+ // the version of the app ID.
+ NSArray *components = [appID componentsSeparatedByString:@":"];
+ if (components.count != 4) {
+ return NO;
+ }
+
+ NSString *suppliedFingerprintString = components[3];
+ if (!suppliedFingerprintString.length) {
+ return NO;
+ }
+
+ uint64_t suppliedFingerprint;
+ NSScanner *scanner = [NSScanner scannerWithString:suppliedFingerprintString];
+ if (![scanner scanHexLongLong:&suppliedFingerprint]) {
+ return NO;
+ }
+
+ if ([version isEqual:@"1:"]) {
+ // The v1 hash algorithm is not permitted on the client so the actual hash cannot be validated.
+ return YES;
+ }
+
+ // Unknown version.
+ return NO;
+}
+
+- (NSString *)expectedBundleID {
+ return _options.bundleID;
+}
+
+// end App ID validation
+#pragma mark
+
+- (void)sendLogsWithServiceName:(NSString *)serviceName
+ version:(NSString *)version
+ error:(NSError *)error{
+ NSMutableDictionary *userInfo = [[NSMutableDictionary alloc] initWithDictionary:@{
+ kFIRAppDiagnosticsConfigurationTypeKey : @(FIRConfigTypeSDK),
+ kFIRAppDiagnosticsSDKNameKey : serviceName,
+ kFIRAppDiagnosticsSDKVersionKey : version,
+ kFIRAppDiagnosticsFIRAppKey : self
+ }];
+ if (error) {
+ userInfo[kFIRAppDiagnosticsErrorKey] = error;
+ }
+ [[NSNotificationCenter defaultCenter]
+ postNotificationName:kFIRAppDiagnosticsNotification
+ object:nil
+ userInfo:userInfo];
+}
+
+@end
diff --git a/Firebase/Core/FIRAppAssociationRegistration.m b/Firebase/Core/FIRAppAssociationRegistration.m
new file mode 100644
index 0000000..a36396d
--- /dev/null
+++ b/Firebase/Core/FIRAppAssociationRegistration.m
@@ -0,0 +1,47 @@
+// Copyright 2017 Google
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "Private/FIRAppAssociationRegistration.h"
+
+#import <objc/runtime.h>
+
+@implementation FIRAppAssociationRegistration
+
++ (nullable id)registeredObjectWithHost:(id)host
+ key:(NSString *)key
+ creationBlock:(id _Nullable (^)())creationBlock {
+ @synchronized(self) {
+ SEL dictKey = @selector(registeredObjectWithHost:key:creationBlock:);
+ NSMutableDictionary<NSString *, id> *objectsByKey = objc_getAssociatedObject(host, dictKey);
+ if (!objectsByKey) {
+ objectsByKey = [[NSMutableDictionary alloc] init];
+ objc_setAssociatedObject(host, dictKey, objectsByKey, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
+ }
+ id obj = objectsByKey[key];
+ NSValue *creationBlockBeingCalled = [NSValue valueWithPointer:dictKey];
+ if (obj) {
+ if ([creationBlockBeingCalled isEqual:obj]) {
+ [NSException raise:@"Reentering registeredObjectWithHost:key:creationBlock: not allowed"
+ format:@"host: %@ key: %@", host, key];
+ }
+ return obj;
+ }
+ objectsByKey[key] = creationBlockBeingCalled;
+ obj = creationBlock();
+ objectsByKey[key] = obj;
+ return obj;
+ }
+}
+
+@end
diff --git a/Firebase/Core/FIRAppEnvironmentUtil.m b/Firebase/Core/FIRAppEnvironmentUtil.m
new file mode 100644
index 0000000..b88b432
--- /dev/null
+++ b/Firebase/Core/FIRAppEnvironmentUtil.m
@@ -0,0 +1,207 @@
+// Copyright 2017 Google
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+
+
+#import "Private/FIRAppEnvironmentUtil.h"
+
+#import <dlfcn.h>
+#import <mach-o/dyld.h>
+#import <sys/utsname.h>
+
+/// The encryption info struct and constants are missing from the iPhoneSimulator SDK, but not from
+/// the iPhoneOS or Mac OS X SDKs. Since one doesn't ever ship a Simulator binary, we'll just
+/// provide the definitions here.
+#if TARGET_IPHONE_SIMULATOR && !defined(LC_ENCRYPTION_INFO)
+#define LC_ENCRYPTION_INFO 0x21
+struct encryption_info_command {
+ uint32_t cmd;
+ uint32_t cmdsize;
+ uint32_t cryptoff;
+ uint32_t cryptsize;
+ uint32_t cryptid;
+};
+#endif
+
+@implementation FIRAppEnvironmentUtil
+
+/// The file name of the sandbox receipt. This is available on iOS >= 8.0
+static NSString *const kFIRAIdentitySandboxReceiptFileName = @"sandboxReceipt";
+
+/// The following copyright from Landon J. Fuller applies to the isAppEncrypted function.
+///
+/// Copyright (c) 2017 Landon J. Fuller <landon@landonf.org>
+/// All rights reserved.
+///
+/// Permission is hereby granted, free of charge, to any person obtaining a copy of this software
+/// and associated documentation files (the "Software"), to deal in the Software without
+/// restriction, including without limitation the rights to use, copy, modify, merge, publish,
+/// distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
+/// Software is furnished to do so, subject to the following conditions:
+///
+/// The above copyright notice and this permission notice shall be included in all copies or
+/// substantial portions of the Software.
+///
+/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
+/// BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+/// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+/// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+///
+/// Comment from <a href="http://iphonedevwiki.net/index.php/Crack_prevention">iPhone Dev Wiki
+/// Crack Prevention</a>:
+/// App Store binaries are signed by both their developer and Apple. This encrypts the binary so
+/// that decryption keys are needed in order to make the binary readable. When iOS executes the
+/// binary, the decryption keys are used to decrypt the binary into a readable state where it is
+/// then loaded into memory and executed. iOS can tell the encryption status of a binary via the
+/// cryptid structure member of LC_ENCRYPTION_INFO MachO load command. If cryptid is a non-zero
+/// value then the binary is encrypted.
+///
+/// 'Cracking' works by letting the kernel decrypt the binary then siphoning the decrypted data into
+/// a new binary file, resigning, and repackaging. This will only work on jailbroken devices as
+/// codesignature validation has been removed. Resigning takes place because while the codesignature
+/// doesn't have to be valid thanks to the jailbreak, it does have to be in place unless you have
+/// AppSync or similar to disable codesignature checks.
+///
+/// More information at <a href="http://landonf.org/2009/02/index.html">Landon Fuller's blog</a>
+static BOOL isAppEncrypted() {
+ const struct mach_header *executableHeader = NULL;
+ for (uint32_t i = 0; i < _dyld_image_count(); i++) {
+ const struct mach_header *header = _dyld_get_image_header(i);
+ if (header && header->filetype == MH_EXECUTE) {
+ executableHeader = header;
+ break;
+ }
+ }
+
+ if (!executableHeader) {
+ return NO;
+ }
+
+ BOOL is64bit = (executableHeader->magic == MH_MAGIC_64);
+ uintptr_t cursor = (uintptr_t)executableHeader +
+ (is64bit ? sizeof(struct mach_header_64) : sizeof(struct mach_header));
+ const struct segment_command *segmentCommand = NULL;
+ uint32_t i = 0;
+
+ while (i++ < executableHeader->ncmds) {
+ segmentCommand = (struct segment_command *)cursor;
+
+ if (!segmentCommand) {
+ continue;
+ }
+
+ if ((!is64bit && segmentCommand->cmd == LC_ENCRYPTION_INFO) ||
+ (is64bit && segmentCommand->cmd == LC_ENCRYPTION_INFO_64)) {
+ if (is64bit) {
+ struct encryption_info_command_64 *cryptCmd =
+ (struct encryption_info_command_64 *)segmentCommand;
+ return cryptCmd && cryptCmd->cryptid != 0;
+ } else {
+ struct encryption_info_command *cryptCmd = (struct encryption_info_command *)segmentCommand;
+ return cryptCmd && cryptCmd->cryptid != 0;
+ }
+ }
+ cursor += segmentCommand->cmdsize;
+ }
+
+ return NO;
+}
+
++ (BOOL)isFromAppStore {
+ static dispatch_once_t isEncryptedOnce;
+ static BOOL isEncrypted = NO;
+
+ dispatch_once(&isEncryptedOnce, ^{
+ isEncrypted = isAppEncrypted();
+ });
+
+ if ([FIRAppEnvironmentUtil isSimulator]) {
+ return NO;
+ }
+ if ([FIRAppEnvironmentUtil hasSCInfoFolder]) {
+ // When iTunes downloads a .ipa, it also gets a customized .sinf file which is added to the
+ // main SC_Info directory.
+ return YES;
+ }
+
+ // For iOS >= 8.0, iTunesMetadata.plist is moved outside of the sandbox. Any attempt to read
+ // the iTunesMetadata.plist outside of the sandbox will be rejected by Apple.
+ // If the app does not contain the sandboxReceipt file which means it is a TestFlight beta, and
+ // it does not contain the embedded.mobileprovision which is stripped out by Apple when the
+ // app is submitted to store, then it is highly likely that it is from Apple Store.
+ return isEncrypted && ![FIRAppEnvironmentUtil isAppStoreReceiptSandbox] &&
+ ![FIRAppEnvironmentUtil hasEmbeddedMobileProvision];
+}
+
++ (BOOL)isAppStoreReceiptSandbox {
+ NSURL *appStoreReceiptURL = [NSBundle mainBundle].appStoreReceiptURL;
+ NSString *appStoreReceiptFileName = appStoreReceiptURL.lastPathComponent;
+ return [appStoreReceiptFileName isEqualToString:kFIRAIdentitySandboxReceiptFileName];
+}
+
++ (BOOL)hasEmbeddedMobileProvision {
+ return [[NSBundle mainBundle] pathForResource:@"embedded" ofType:@"mobileprovision"].length > 0;
+}
+
++ (BOOL)isSimulator {
+ NSString *platform = [FIRAppEnvironmentUtil deviceModel];
+ return [platform isEqual:@"x86_64"] || [platform isEqual:@"i386"];
+}
+
++ (NSString *)deviceModel {
+ static dispatch_once_t once;
+ static NSString *deviceModel;
+
+ dispatch_once(&once, ^{
+ struct utsname systemInfo;
+ if (uname(&systemInfo) == 0) {
+ deviceModel = [NSString stringWithUTF8String:systemInfo.machine];
+ }
+ });
+ return deviceModel;
+}
+
++ (NSString *)systemVersion {
+ return [UIDevice currentDevice].systemVersion;
+}
+
++ (BOOL)isAppExtension {
+ // Documented by <a href="https://goo.gl/RRB2Up">Apple</a>
+ BOOL appExtension = [[[NSBundle mainBundle] bundlePath] hasSuffix:@".appex"];
+ return appExtension;
+}
+
++ (UIApplication *)sharedApplication {
+ if ([FIRAppEnvironmentUtil isAppExtension]) {
+ return nil;
+ }
+ id sharedApplication = nil;
+ Class uiApplicationClass = NSClassFromString(@"UIApplication");
+ if (uiApplicationClass &&
+ [uiApplicationClass respondsToSelector:(NSSelectorFromString(@"sharedApplication"))]) {
+ sharedApplication = [uiApplicationClass sharedApplication];
+ }
+ return sharedApplication;
+}
+
+#pragma mark - Helper methods
+
++ (BOOL)hasSCInfoFolder {
+ NSString *bundlePath = [NSBundle mainBundle].bundlePath;
+ NSString *scInfoPath = [bundlePath stringByAppendingPathComponent:@"SC_Info"];
+ return [[NSFileManager defaultManager] fileExistsAtPath:scInfoPath];
+}
+
+@end
diff --git a/Firebase/Core/FIRBundleUtil.m b/Firebase/Core/FIRBundleUtil.m
new file mode 100644
index 0000000..6c1d1e9
--- /dev/null
+++ b/Firebase/Core/FIRBundleUtil.m
@@ -0,0 +1,65 @@
+// Copyright 2017 Google
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "Private/FIRBundleUtil.h"
+
+@implementation FIRBundleUtil
+
++ (NSArray *)relevantBundles {
+ return @[ [NSBundle mainBundle], [NSBundle bundleForClass:[self class]] ];
+}
+
++ (NSString *)optionsDictionaryPathWithResourceName:(NSString *)resourceName
+ andFileType:(NSString *)fileType
+ inBundles:(NSArray *)bundles {
+ // Loop through all bundles to find the config dict.
+ for (NSBundle *bundle in bundles) {
+ NSString *path = [bundle pathForResource:resourceName ofType:fileType];
+ // Use the first one we find.
+ if (path) {
+ return path;
+ }
+ }
+ return nil;
+}
+
++ (NSArray *)relevantURLSchemes {
+ NSMutableArray *result = [[NSMutableArray alloc] init];
+ for (NSBundle *bundle in [[self class] relevantBundles]) {
+ NSArray *urlTypes = [bundle objectForInfoDictionaryKey:@"CFBundleURLTypes"];
+ for (NSDictionary *urlType in urlTypes) {
+ [result addObjectsFromArray:urlType[@"CFBundleURLSchemes"]];
+ }
+ }
+ return result;
+}
+
++ (NSSet *)relevantBundleIdentifiers {
+ NSMutableSet *result = [[NSMutableSet alloc] init];
+ for (NSBundle *bundle in [[self class] relevantBundles]) {
+ [result addObject:[bundle bundleIdentifier]];
+ }
+ return result;
+}
+
++ (BOOL)hasBundleIdentifier:(NSString *)bundleIdentifier inBundles:(NSArray *)bundles {
+ for (NSBundle *bundle in bundles) {
+ if ([bundle.bundleIdentifier isEqualToString:bundleIdentifier]) {
+ return YES;
+ }
+ }
+ return NO;
+}
+
+@end
diff --git a/Firebase/Core/FIRConfiguration.h b/Firebase/Core/FIRConfiguration.h
new file mode 100644
index 0000000..496b211
--- /dev/null
+++ b/Firebase/Core/FIRConfiguration.h
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+#import "FIRAnalyticsConfiguration.h"
+#import "FIRCoreSwiftNameSupport.h"
+#import "FIRLoggerLevel.h"
+
+/**
+ * The log levels used by FIRConfiguration.
+ */
+typedef NS_ENUM(NSInteger, FIRLogLevel) {
+ /** Error */
+ kFIRLogLevelError __deprecated = 0,
+ /** Warning */
+ kFIRLogLevelWarning __deprecated,
+ /** Info */
+ kFIRLogLevelInfo __deprecated,
+ /** Debug */
+ kFIRLogLevelDebug __deprecated,
+ /** Assert */
+ kFIRLogLevelAssert __deprecated,
+ /** Max */
+ kFIRLogLevelMax __deprecated = kFIRLogLevelAssert
+} DEPRECATED_MSG_ATTRIBUTE(
+ "Use -FIRDebugEnabled and -FIRDebugDisabled or setLoggerLevel. See FIRApp.h for more details.");
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * This interface provides global level properties that the developer can tweak, and the singleton
+ * of the Firebase Analytics configuration class.
+ */
+FIR_SWIFT_NAME(FirebaseConfiguration)
+@interface FIRConfiguration : NSObject
+
+
+#if defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0
+/** Returns the shared configuration object. */
+@property(class, nonatomic, readonly) FIRConfiguration *sharedInstance FIR_SWIFT_NAME(shared);
+#else
+/** Returns the shared configuration object. */
++ (FIRConfiguration *)sharedInstance FIR_SWIFT_NAME(shared());
+#endif // defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0
+
+/** The configuration class for Firebase Analytics. */
+@property(nonatomic, readwrite) FIRAnalyticsConfiguration *analyticsConfiguration;
+
+/** Global log level. Defaults to kFIRLogLevelError. */
+@property(nonatomic, readwrite, assign) FIRLogLevel logLevel DEPRECATED_MSG_ATTRIBUTE(
+ "Use -FIRDebugEnabled and -FIRDebugDisabled or setLoggerLevel. See FIRApp.h for more details.");
+
+/**
+ * Sets the logging level for internal Firebase logging. Firebase will only log messages
+ * that are logged at or below loggerLevel. The messages are logged both to the Xcode
+ * console and to the device's log. Note that if an app is running from AppStore, it will
+ * never log above FIRLoggerLevelNotice even if loggerLevel is set to a higher (more verbose)
+ * setting.
+ *
+ * @param loggerLevel The maximum logging level. The default level is set to FIRLoggerLevelNotice.
+ */
+- (void)setLoggerLevel:(FIRLoggerLevel)loggerLevel;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Core/FIRConfiguration.m b/Firebase/Core/FIRConfiguration.m
new file mode 100644
index 0000000..921aa48
--- /dev/null
+++ b/Firebase/Core/FIRConfiguration.m
@@ -0,0 +1,51 @@
+// Copyright 2017 Google
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "FIRConfiguration.h"
+
+extern void FIRSetLoggerLevel(FIRLoggerLevel loggerLevel);
+
+@implementation FIRConfiguration
+
++ (instancetype)sharedInstance {
+ static FIRConfiguration *sharedInstance = nil;
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ sharedInstance = [[FIRConfiguration alloc] init];
+ });
+ return sharedInstance;
+}
+
+- (instancetype)init {
+ self = [super init];
+ if (self) {
+ _analyticsConfiguration = [FIRAnalyticsConfiguration sharedInstance];
+ }
+ return self;
+}
+
+// This is deprecated, use setLoggerLevel instead.
+- (void)setLogLevel:(FIRLogLevel)logLevel {
+ NSAssert(logLevel <= kFIRLogLevelMax, @"Invalid log level, %ld", (long)logLevel);
+ _logLevel = logLevel;
+}
+
+- (void)setLoggerLevel:(FIRLoggerLevel)loggerLevel {
+ NSAssert(loggerLevel <= FIRLoggerLevelMax && loggerLevel >= FIRLoggerLevelMin,
+ @"Invalid logger level, %ld", (long)loggerLevel);
+ FIRSetLoggerLevel(loggerLevel);
+}
+
+
+@end
diff --git a/Firebase/Core/FIRCoreSwiftNameSupport.h b/Firebase/Core/FIRCoreSwiftNameSupport.h
new file mode 100644
index 0000000..f58bdd7
--- /dev/null
+++ b/Firebase/Core/FIRCoreSwiftNameSupport.h
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef FIR_SWIFT_NAME
+
+#import <Foundation/Foundation.h>
+
+// NS_SWIFT_NAME can only translate factory methods before the iOS 9.3 SDK.
+// // Wrap it in our own macro if it's a non-compatible SDK.
+#ifdef __IPHONE_9_3
+#define FIR_SWIFT_NAME(X) NS_SWIFT_NAME(X)
+#else
+#define FIR_SWIFT_NAME(X) // Intentionally blank.
+#endif // #ifdef __IPHONE_9_3
+
+#endif // FIR_SWIFT_NAME
diff --git a/Firebase/Core/FIRErrors.m b/Firebase/Core/FIRErrors.m
new file mode 100644
index 0000000..3c7e39a
--- /dev/null
+++ b/Firebase/Core/FIRErrors.m
@@ -0,0 +1,33 @@
+// Copyright 2017 Google
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "Private/FIRErrors.h"
+
+NSString *const kFirebaseErrorDomain = @"com.firebase";
+NSString *const kFirebaseAdMobErrorDomain = @"com.firebase.admob";
+NSString *const kFirebaseAppInviteErrorDomain = @"com.firebase.appinvite";
+NSString *const kFirebaseAuthErrorDomain = @"com.firebase.auth";
+NSString *const kFirebaseCloudMessagingErrorDomain = @"com.firebase.cloudmessaging";
+NSString *const kFirebaseConfigErrorDomain = @"com.firebase.config";
+NSString *const kFirebaseCoreErrorDomain = @"com.firebase.core";
+NSString *const kFirebaseCrashReportingErrorDomain = @"com.firebase.crashreporting";
+NSString *const kFirebaseDatabaseErrorDomain = @"com.firebase.database";
+NSString *const kFirebaseDurableDeepLinkErrorDomain = @"com.firebase.durabledeeplink";
+NSString *const kFirebaseInstanceIDErrorDomain = @"com.firebase.instanceid";
+NSString *const kFirebasePerfErrorDomain = @"com.firebase.perf";
+NSString *const kFirebaseStorageErrorDomain = @"com.firebase.storage";
+
+NSError *FIRCreateError(NSString *domain, enum FIRErrorCode code, NSDictionary *userInfo) {
+ return [NSError errorWithDomain:domain code:code userInfo:userInfo];
+}
diff --git a/Firebase/Core/FIRLogger.m b/Firebase/Core/FIRLogger.m
new file mode 100644
index 0000000..0e3e325
--- /dev/null
+++ b/Firebase/Core/FIRLogger.m
@@ -0,0 +1,228 @@
+// Copyright 2017 Google
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "Private/FIRLogger.h"
+
+#import "FIRLoggerLevel.h"
+#import "Private/FIRAppEnvironmentUtil.h"
+
+#include <asl.h>
+#include <assert.h>
+#include <stdbool.h>
+#include <sys/types.h>
+#include <unistd.h>
+#include <sys/sysctl.h>
+
+FIRLoggerService kFIRLoggerABTesting = @"[Firebase/ABTesting]";
+FIRLoggerService kFIRLoggerAdMob = @"[Firebase/AdMob]";
+FIRLoggerService kFIRLoggerAnalytics = @"[Firebase/Analytics]";
+FIRLoggerService kFIRLoggerAuth = @"[Firebase/Auth]";
+FIRLoggerService kFIRLoggerCore = @"[Firebase/Core]";
+FIRLoggerService kFIRLoggerCrash = @"[Firebase/Crash]";
+FIRLoggerService kFIRLoggerDatabase = @"[Firebase/Database]";
+FIRLoggerService kFIRLoggerDynamicLinks = @"[Firebase/DynamicLinks]";
+FIRLoggerService kFIRLoggerInstanceID = @"[Firebase/InstanceID]";
+FIRLoggerService kFIRLoggerInvites = @"[Firebase/Invites]";
+FIRLoggerService kFIRLoggerMessaging = @"[Firebase/Messaging]";
+FIRLoggerService kFIRLoggerPerf = @"[Firebase/Performance]";
+FIRLoggerService kFIRLoggerRemoteConfig = @"[Firebase/RemoteConfig]";
+FIRLoggerService kFIRLoggerStorage = @"[Firebase/Storage]";
+
+/// Arguments passed on launch.
+NSString *const kFIRDisableDebugModeApplicationArgument = @"-FIRDebugDisabled";
+NSString *const kFIREnableDebugModeApplicationArgument = @"-FIRDebugEnabled";
+
+/// Key for the debug mode bit in NSUserDefaults.
+NSString *const kFIRPersistedDebugModeKey = @"/google/firebase/debug_mode";
+
+/// ASL client facility name used by FIRLogger.
+const char *kFIRLoggerASLClientFacilityName = "com.firebase.app.logger";
+
+/// Message format used by ASL client that matches format of NSLog.
+const char *kFIRLoggerCustomASLMessageFormat =
+ "$((Time)(J.3)) $(Sender)[$(PID)] <$((Level)(str))> $Message";
+
+static dispatch_once_t sFIRLoggerOnceToken;
+
+static aslclient sFIRLoggerClient;
+
+static dispatch_queue_t sFIRClientQueue;
+
+static BOOL sFIRLoggerDebugMode;
+
+// The sFIRAnalyticsDebugMode flag is here to support the -FIRDebugEnabled/-FIRDebugDisabled
+// flags used by Analytics. Users who use those flags expect Analytics to log verbosely,
+// while the rest of Firebase logs at the default level. This flag is introduced to support
+// that behavior.
+static BOOL sFIRAnalyticsDebugMode;
+
+static FIRLoggerLevel sFIRLoggerMaximumLevel;
+
+/// The regex pattern for the message code.
+static NSString *const kMessageCodePattern = @"^I-[A-Z]{3}[0-9]{6}$";
+static NSRegularExpression *sMessageCodeRegex;
+
+void FIRLoggerInitializeASL() {
+ dispatch_once(&sFIRLoggerOnceToken, ^{
+ // Initialize the ASL client handle.
+ sFIRLoggerClient = asl_open(NULL, kFIRLoggerASLClientFacilityName, ASL_OPT_STDERR);
+
+ // Set the filter used by system/device log. Initialize in default mode.
+ asl_set_filter(sFIRLoggerClient, ASL_FILTER_MASK_UPTO(ASL_LEVEL_NOTICE));
+ sFIRLoggerDebugMode = NO;
+ sFIRAnalyticsDebugMode = NO;
+ sFIRLoggerMaximumLevel = FIRLoggerLevelNotice;
+
+ NSArray *arguments = [NSProcessInfo processInfo].arguments;
+ NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
+ BOOL debugMode = [userDefaults boolForKey:kFIRPersistedDebugModeKey];
+
+ if ([arguments containsObject:kFIRDisableDebugModeApplicationArgument]) { // Default mode
+ [userDefaults removeObjectForKey:kFIRPersistedDebugModeKey];
+ } else if ([arguments containsObject:kFIREnableDebugModeApplicationArgument]
+ || debugMode) { // Debug mode
+ [userDefaults setBool:YES forKey:kFIRPersistedDebugModeKey];
+ asl_set_filter(sFIRLoggerClient, ASL_FILTER_MASK_UPTO(ASL_LEVEL_DEBUG));
+ sFIRLoggerDebugMode = YES;
+ }
+
+ // We should disable debug mode if we are running from App Store.
+ if (sFIRLoggerDebugMode && [FIRAppEnvironmentUtil isFromAppStore]) {
+ sFIRLoggerDebugMode = NO;
+ }
+
+ // Need to call asl_add_output_file so that the logs can appear in Xcode's console view. Set
+ // the ASL filter mask for this output file up to debug level so that all messages are viewable
+ // in the console.
+ asl_add_output_file(sFIRLoggerClient, STDERR_FILENO, kFIRLoggerCustomASLMessageFormat,
+ ASL_TIME_FMT_LCL, ASL_FILTER_MASK_UPTO(ASL_LEVEL_DEBUG), ASL_ENCODE_SAFE);
+
+ sFIRClientQueue = dispatch_queue_create("FIRLoggingClientQueue", DISPATCH_QUEUE_SERIAL);
+ dispatch_set_target_queue(sFIRClientQueue,
+ dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0));
+
+ sMessageCodeRegex =
+ [NSRegularExpression regularExpressionWithPattern:kMessageCodePattern options:0 error:NULL];
+ });
+}
+
+void FIRSetAnalyticsDebugMode(BOOL analyticsDebugMode) {
+ FIRLoggerInitializeASL();
+ dispatch_async(sFIRClientQueue, ^{
+ // We should not enable debug mode if we are running from App Store.
+ if (analyticsDebugMode && [FIRAppEnvironmentUtil isFromAppStore]) {
+ return;
+ }
+ sFIRAnalyticsDebugMode = analyticsDebugMode;
+ asl_set_filter(sFIRLoggerClient, ASL_FILTER_MASK_UPTO(ASL_LEVEL_DEBUG));
+ });
+}
+
+void FIRSetLoggerLevel(FIRLoggerLevel loggerLevel) {
+ if (loggerLevel < FIRLoggerLevelMin || loggerLevel > FIRLoggerLevelMax) {
+ FIRLogError(kFIRLoggerCore, @"I-COR000023", @"Invalid logger level, %ld", (long)loggerLevel);
+ return;
+ }
+ FIRLoggerInitializeASL();
+ dispatch_async(sFIRClientQueue, ^{
+ // We should not raise the logger level if we are running from App Store.
+ if (loggerLevel >= FIRLoggerLevelNotice && [FIRAppEnvironmentUtil isFromAppStore]) {
+ return;
+ }
+
+ sFIRLoggerMaximumLevel = loggerLevel;
+ asl_set_filter(sFIRLoggerClient, ASL_FILTER_MASK_UPTO(loggerLevel));
+ });
+}
+
+BOOL FIRIsLoggableLevel(FIRLoggerLevel loggerLevel, BOOL analyticsComponent) {
+ FIRLoggerInitializeASL();
+ if (sFIRLoggerDebugMode) {
+ return YES;
+ } else if (sFIRAnalyticsDebugMode && analyticsComponent) {
+ return YES;
+ }
+ return (BOOL)(loggerLevel <= sFIRLoggerMaximumLevel);
+}
+
+#ifdef DEBUG
+void FIRResetLogger() {
+ sFIRLoggerOnceToken = 0;
+ [[NSUserDefaults standardUserDefaults] removeObjectForKey:kFIRPersistedDebugModeKey];
+}
+
+aslclient getFIRLoggerClient() {
+ return sFIRLoggerClient;
+}
+
+dispatch_queue_t getFIRClientQueue() {
+ return sFIRClientQueue;
+}
+
+BOOL getFIRLoggerDebugMode() {
+ return sFIRLoggerDebugMode;
+}
+#endif
+
+void FIRLogBasic(FIRLoggerLevel level, FIRLoggerService service, NSString *messageCode,
+ NSString *message, va_list args_ptr) {
+ FIRLoggerInitializeASL();
+ BOOL canLog = level <= sFIRLoggerMaximumLevel;
+
+ if (sFIRLoggerDebugMode) {
+ canLog = YES;
+ } else if (sFIRAnalyticsDebugMode && [kFIRLoggerAnalytics isEqualToString:service]) {
+ canLog = YES;
+ }
+
+ if (!canLog) {
+ return;
+ }
+#ifdef DEBUG
+ NSCAssert(messageCode.length == 11, @"Incorrect message code length.");
+ NSRange messageCodeRange = NSMakeRange(0, messageCode.length);
+ NSUInteger numberOfMatches =
+ [sMessageCodeRegex numberOfMatchesInString:messageCode options:0 range:messageCodeRange];
+ NSCAssert(numberOfMatches == 1, @"Incorrect message code format.");
+#endif
+ NSString *logMsg = [[NSString alloc] initWithFormat:message arguments:args_ptr];
+ logMsg = [NSString stringWithFormat:@"%@[%@] %@", service, messageCode, logMsg];
+ dispatch_async(sFIRClientQueue, ^{
+ asl_log(sFIRLoggerClient, NULL, level, "%s", logMsg.UTF8String);
+ });
+}
+
+/**
+ * Generates the logging functions using macros.
+ *
+ * Calling FIRLogError(kFIRLoggerCore, @"I-COR000001", @"Configure %@ failed.", @"blah") shows:
+ * yyyy-mm-dd hh:mm:ss.SSS sender[PID] <Error> [Firebase/Core][I-COR000001] Configure blah failed.
+ * Calling FIRLogDebug(kFIRLoggerCore, @"I-COR000001", @"Configure succeed.") shows:
+ * yyyy-mm-dd hh:mm:ss.SSS sender[PID] <Debug> [Firebase/Core][I-COR000001] Configure succeed.
+ */
+#define FIR_LOGGING_FUNCTION(level) \
+void FIRLog##level(FIRLoggerService service, NSString *messageCode, NSString *message, ...) { \
+ va_list args_ptr; \
+ va_start(args_ptr, message); \
+ FIRLogBasic(FIRLoggerLevel##level, service, messageCode, message, args_ptr); \
+ va_end(args_ptr); \
+}
+
+FIR_LOGGING_FUNCTION(Error)
+FIR_LOGGING_FUNCTION(Warning)
+FIR_LOGGING_FUNCTION(Notice)
+FIR_LOGGING_FUNCTION(Info)
+FIR_LOGGING_FUNCTION(Debug)
+
+#undef FIR_MAKE_LOGGER
diff --git a/Firebase/Core/FIRLoggerLevel.h b/Firebase/Core/FIRLoggerLevel.h
new file mode 100644
index 0000000..fe0d47d
--- /dev/null
+++ b/Firebase/Core/FIRLoggerLevel.h
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRCoreSwiftNameSupport.h"
+
+/**
+ * The log levels used by internal logging.
+ */
+typedef NS_ENUM(NSInteger, FIRLoggerLevel) {
+ FIRLoggerLevelError = 3 /*ASL_LEVEL_ERR*/,
+ FIRLoggerLevelWarning = 4 /*ASL_LEVEL_WARNING*/,
+ FIRLoggerLevelNotice = 5 /*ASL_LEVEL_NOTICE*/,
+ FIRLoggerLevelInfo = 6 /*ASL_LEVEL_INFO*/,
+ FIRLoggerLevelDebug = 7 /*ASL_LEVEL_DEBUG*/,
+ FIRLoggerLevelMin = FIRLoggerLevelError,
+ FIRLoggerLevelMax = FIRLoggerLevelDebug
+} FIR_SWIFT_NAME(FirebaseLoggerLevel);
diff --git a/Firebase/Core/FIRMutableDictionary.m b/Firebase/Core/FIRMutableDictionary.m
new file mode 100644
index 0000000..1d6ef3a
--- /dev/null
+++ b/Firebase/Core/FIRMutableDictionary.m
@@ -0,0 +1,97 @@
+// Copyright 2017 Google
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "Private/FIRMutableDictionary.h"
+
+@implementation FIRMutableDictionary {
+ /// The mutable dictionary.
+ NSMutableDictionary *_objects;
+
+ /// Serial synchronization queue. All reads should use dispatch_sync, while writes use
+ /// dispatch_async.
+ dispatch_queue_t _queue;
+}
+
+- (instancetype)init {
+ self = [super init];
+
+ if (self) {
+ _objects = [[NSMutableDictionary alloc] init];
+ _queue = dispatch_queue_create("FIRMutableDictionary", DISPATCH_QUEUE_SERIAL);
+ }
+
+ return self;
+}
+
+- (NSString *)description {
+ __block NSString *description;
+ dispatch_sync(_queue, ^{
+ description = _objects.description;
+ });
+ return description;
+}
+
+- (id)objectForKey:(id)key {
+ __block id object;
+ dispatch_sync(_queue, ^{
+ object = _objects[key];
+ });
+ return object;
+}
+
+- (void)setObject:(id)object forKey:(id<NSCopying>)key {
+ dispatch_async(_queue, ^{
+ _objects[key] = object;
+ });
+}
+
+- (void)removeObjectForKey:(id)key {
+ dispatch_async(_queue, ^{
+ [_objects removeObjectForKey:key];
+ });
+}
+
+- (void)removeAllObjects {
+ dispatch_async(_queue, ^{
+ [_objects removeAllObjects];
+ });
+}
+
+- (NSUInteger)count {
+ __block NSUInteger count;
+ dispatch_sync(_queue, ^{
+ count = _objects.count;
+ });
+ return count;
+}
+
+- (id)objectForKeyedSubscript:(id<NSCopying>)key {
+ // The method this calls is already synchronized.
+ return [self objectForKey:key];
+}
+
+- (void)setObject:(id)obj forKeyedSubscript:(id<NSCopying>)key {
+ // The method this calls is already synchronized.
+ [self setObject:obj forKey:key];
+}
+
+- (NSDictionary *)dictionary {
+ __block NSDictionary *dictionary;
+ dispatch_sync(_queue, ^{
+ dictionary = [_objects copy];
+ });
+ return dictionary;
+}
+
+@end
diff --git a/Firebase/Core/FIRNetwork.m b/Firebase/Core/FIRNetwork.m
new file mode 100644
index 0000000..4926b2f
--- /dev/null
+++ b/Firebase/Core/FIRNetwork.m
@@ -0,0 +1,390 @@
+// Copyright 2017 Google
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "Private/FIRNetwork.h"
+#import "Private/FIRNetworkMessageCode.h"
+
+#import "Private/FIRMutableDictionary.h"
+#import "Private/FIRNetworkConstants.h"
+#import "Private/FIRReachabilityChecker.h"
+#import "Private/FIRLogger.h"
+
+#import <GoogleToolboxForMac/GTMNSData+zlib.h>
+
+/// Constant string for request header Content-Encoding.
+static NSString *const kFIRNetworkContentCompressionKey = @"Content-Encoding";
+
+/// Constant string for request header Content-Encoding value.
+static NSString *const kFIRNetworkContentCompressionValue = @"gzip";
+
+/// Constant string for request header Content-Length.
+static NSString *const kFIRNetworkContentLengthKey = @"Content-Length";
+
+/// Constant string for request header Content-Type.
+static NSString *const kFIRNetworkContentTypeKey = @"Content-Type";
+
+/// Constant string for request header Content-Type value.
+static NSString *const kFIRNetworkContentTypeValue = @"application/x-www-form-urlencoded";
+
+/// Constant string for GET request method.
+static NSString *const kFIRNetworkGETRequestMethod = @"GET";
+
+/// Constant string for POST request method.
+static NSString *const kFIRNetworkPOSTRequestMethod = @"POST";
+
+/// Default constant string as a prefix for network logger.
+static NSString *const kFIRNetworkLogTag = @"Firebase/Network";
+
+@interface FIRNetwork ()<FIRReachabilityDelegate, FIRNetworkLoggerDelegate>
+@end
+
+@implementation FIRNetwork {
+ /// Network reachability.
+ FIRReachabilityChecker *_reachability;
+
+ /// The dictionary of requests by session IDs { NSString : id }.
+ FIRMutableDictionary *_requests;
+}
+
+- (instancetype)init {
+ return [self initWithReachabilityHost:kFIRNetworkReachabilityHost];
+}
+
+- (instancetype)initWithReachabilityHost:(NSString *)reachabilityHost {
+ self = [super init];
+ if (self) {
+ // Setup reachability.
+ _reachability =
+ [[FIRReachabilityChecker alloc] initWithReachabilityDelegate:self
+ loggerDelegate:self
+ withHost:reachabilityHost];
+ if (![_reachability start]) {
+ return nil;
+ }
+
+ _requests = [[FIRMutableDictionary alloc] init];
+ _timeoutInterval = kFIRNetworkTimeOutInterval;
+ }
+ return self;
+}
+
+- (void)dealloc {
+ _reachability.reachabilityDelegate = nil;
+ [_reachability stop];
+}
+
+#pragma mark - External Methods
+
++ (void)handleEventsForBackgroundURLSessionID:(NSString *)sessionID
+ completionHandler:(FIRNetworkSystemCompletionHandler)completionHandler {
+ [FIRNetworkURLSession handleEventsForBackgroundURLSessionID:sessionID
+ completionHandler:completionHandler];
+}
+
+- (NSString *)postURL:(NSURL *)url
+ payload:(NSData *)payload
+ queue:(dispatch_queue_t)queue
+ usingBackgroundSession:(BOOL)usingBackgroundSession
+ completionHandler:(FIRNetworkCompletionHandler)handler {
+ if (!url.absoluteString.length) {
+ [self handleErrorWithCode:FIRErrorCodeNetworkInvalidURL queue:queue withHandler:handler];
+ return nil;
+ }
+
+ NSTimeInterval timeOutInterval = _timeoutInterval ?: kFIRNetworkTimeOutInterval;
+
+ NSMutableURLRequest *request =
+ [[NSMutableURLRequest alloc] initWithURL:url
+ cachePolicy:NSURLRequestReloadIgnoringLocalCacheData
+ timeoutInterval:timeOutInterval];
+
+ if (!request) {
+ [self handleErrorWithCode:FIRErrorCodeNetworkSessionTaskCreation
+ queue:queue
+ withHandler:handler];
+ return nil;
+ }
+
+ NSError *compressError = nil;
+ NSData *compressedData = [NSData gtm_dataByGzippingData:payload error:&compressError];
+ if (!compressedData || compressError) {
+ if (compressError || payload.length > 0) {
+ // If the payload is not empty but it fails to compress the payload, something has been wrong.
+ [self handleErrorWithCode:FIRErrorCodeNetworkPayloadCompression
+ queue:queue
+ withHandler:handler];
+ return nil;
+ }
+ compressedData = [[NSData alloc] init];
+ }
+
+ NSString *postLength = @(compressedData.length).stringValue;
+
+ // Set up the request with the compressed data.
+ [request setValue:postLength forHTTPHeaderField:kFIRNetworkContentLengthKey];
+ request.HTTPBody = compressedData;
+ request.HTTPMethod = kFIRNetworkPOSTRequestMethod;
+ [request setValue:kFIRNetworkContentTypeValue forHTTPHeaderField:kFIRNetworkContentTypeKey];
+ [request setValue:kFIRNetworkContentCompressionValue
+ forHTTPHeaderField:kFIRNetworkContentCompressionKey];
+
+ FIRNetworkURLSession *fetcher = [[FIRNetworkURLSession alloc] initWithNetworkLoggerDelegate:self];
+ fetcher.backgroundNetworkEnabled = usingBackgroundSession;
+
+ __weak FIRNetwork *weakSelf = self;
+ NSString *requestID = [fetcher
+ sessionIDFromAsyncPOSTRequest:request
+ completionHandler:^(NSHTTPURLResponse *response, NSData *data,
+ NSString *sessionID, NSError *error) {
+ FIRNetwork *strongSelf = weakSelf;
+ if (!strongSelf) {
+ return;
+ }
+ dispatch_queue_t queueToDispatch = queue ? queue : dispatch_get_main_queue();
+ dispatch_async(queueToDispatch, ^{
+ if (sessionID.length) {
+ [strongSelf->_requests removeObjectForKey:sessionID];
+ }
+ if (handler) {
+ handler(response, data, error);
+ }
+ });
+ }];
+ if (!requestID) {
+ [self handleErrorWithCode:FIRErrorCodeNetworkSessionTaskCreation
+ queue:queue
+ withHandler:handler];
+ return nil;
+ }
+
+ [self firNetwork_logWithLevel:kFIRNetworkLogLevelDebug
+ messageCode:kFIRNetworkMessageCodeNetwork000
+ message:@"Uploading data. Host"
+ context:url];
+ _requests[requestID] = fetcher;
+ return requestID;
+}
+
+- (NSString *)getURL:(NSURL *)url
+ headers:(NSDictionary *)headers
+ queue:(dispatch_queue_t)queue
+ usingBackgroundSession:(BOOL)usingBackgroundSession
+ completionHandler:(FIRNetworkCompletionHandler)handler {
+ if (!url.absoluteString.length) {
+ [self handleErrorWithCode:FIRErrorCodeNetworkInvalidURL queue:queue withHandler:handler];
+ return nil;
+ }
+
+ NSTimeInterval timeOutInterval = _timeoutInterval ?: kFIRNetworkTimeOutInterval;
+ NSMutableURLRequest *request =
+ [[NSMutableURLRequest alloc] initWithURL:url
+ cachePolicy:NSURLRequestReloadIgnoringLocalCacheData
+ timeoutInterval:timeOutInterval];
+
+ if (!request) {
+ [self handleErrorWithCode:FIRErrorCodeNetworkSessionTaskCreation
+ queue:queue
+ withHandler:handler];
+ return nil;
+ }
+
+ request.HTTPMethod = kFIRNetworkGETRequestMethod;
+ request.allHTTPHeaderFields = headers;
+
+ FIRNetworkURLSession *fetcher = [[FIRNetworkURLSession alloc] initWithNetworkLoggerDelegate:self];
+ fetcher.backgroundNetworkEnabled = usingBackgroundSession;
+
+ __weak FIRNetwork *weakSelf = self;
+ NSString *requestID = [fetcher
+ sessionIDFromAsyncGETRequest:request
+ completionHandler:^(NSHTTPURLResponse *response, NSData *data, NSString *sessionID,
+ NSError *error) {
+ FIRNetwork *strongSelf = weakSelf;
+ if (!strongSelf) {
+ return;
+ }
+ dispatch_queue_t queueToDispatch = queue ? queue : dispatch_get_main_queue();
+ dispatch_async(queueToDispatch, ^{
+ if (sessionID.length) {
+ [strongSelf->_requests removeObjectForKey:sessionID];
+ }
+ if (handler) {
+ handler(response, data, error);
+ }
+ });
+ }];
+
+ if (!requestID) {
+ [self handleErrorWithCode:FIRErrorCodeNetworkSessionTaskCreation
+ queue:queue
+ withHandler:handler];
+ return nil;
+ }
+
+ [self firNetwork_logWithLevel:kFIRNetworkLogLevelDebug
+ messageCode:kFIRNetworkMessageCodeNetwork001
+ message:@"Downloading data. Host"
+ context:url];
+ _requests[requestID] = fetcher;
+ return requestID;
+}
+
+- (BOOL)hasUploadInProgress {
+ return _requests.count > 0;
+}
+
+#pragma mark - Network Reachability
+
+/// Tells reachability delegate to call reachabilityDidChangeToStatus: to notify the network
+/// reachability has changed.
+- (void)reachability:(FIRReachabilityChecker *)reachability
+ statusChanged:(FIRReachabilityStatus)status {
+ _networkConnected = (status == kFIRReachabilityViaCellular || status == kFIRReachabilityViaWifi);
+ [_reachabilityDelegate reachabilityDidChange];
+}
+
+#pragma mark - Network logger delegate
+
+- (void)setLoggerDelegate:(id<FIRNetworkLoggerDelegate>)loggerDelegate {
+ // Explicitly check whether the delegate responds to the methods because conformsToProtocol does
+ // not work correctly even though the delegate does respond to the methods.
+ if (!loggerDelegate ||
+ ![loggerDelegate
+ respondsToSelector:@selector(firNetwork_logWithLevel:messageCode:message:contexts:)] ||
+ ![loggerDelegate
+ respondsToSelector:@selector(firNetwork_logWithLevel:messageCode:message:context:)] ||
+ ![loggerDelegate
+ respondsToSelector:@selector(firNetwork_logWithLevel:messageCode:message:)]) {
+ FIRLogError(kFIRLoggerAnalytics,
+ [NSString stringWithFormat:@"I-NET%06ld", (long)kFIRNetworkMessageCodeNetwork002],
+ @"Cannot set the network logger delegate: delegate does not conform to the network "
+ "logger protocol.");
+ return;
+ }
+ _loggerDelegate = loggerDelegate;
+}
+
+#pragma mark - Private methods
+
+/// Handles network error and calls completion handler with the error.
+- (void)handleErrorWithCode:(NSInteger)code
+ queue:(dispatch_queue_t)queue
+ withHandler:(FIRNetworkCompletionHandler)handler {
+ NSDictionary *userInfo = @{ kFIRNetworkErrorContext : @"Failed to create network request" };
+ NSError *error =
+ [[NSError alloc] initWithDomain:kFIRNetworkErrorDomain code:code userInfo:userInfo];
+ [self firNetwork_logWithLevel:kFIRNetworkLogLevelWarning
+ messageCode:kFIRNetworkMessageCodeNetwork002
+ message:@"Failed to create network request. Code, error"
+ contexts:@[ @(code), error ]];
+ if (handler) {
+ dispatch_queue_t queueToDispatch = queue ? queue : dispatch_get_main_queue();
+ dispatch_async(queueToDispatch, ^{
+ handler(nil, nil, error);
+ });
+ }
+}
+
+#pragma mark - Network logger
+
+- (void)firNetwork_logWithLevel:(FIRNetworkLogLevel)logLevel
+ messageCode:(FIRNetworkMessageCode)messageCode
+ message:(NSString *)message
+ contexts:(NSArray *)contexts {
+ // Let the delegate log the message if there is a valid logger delegate. Otherwise, just log
+ // errors/warnings/info messages to the console log.
+ if (_loggerDelegate) {
+ [_loggerDelegate firNetwork_logWithLevel:logLevel
+ messageCode:messageCode
+ message:message
+ contexts:contexts];
+ return;
+ }
+ if (_isDebugModeEnabled || logLevel == kFIRNetworkLogLevelError ||
+ logLevel == kFIRNetworkLogLevelWarning || logLevel == kFIRNetworkLogLevelInfo) {
+ NSString *formattedMessage = FIRStringWithLogMessage(message, logLevel, contexts);
+ NSLog(@"%@", formattedMessage);
+ FIRLogBasic((FIRLoggerLevel)logLevel, kFIRLoggerCore,
+ [NSString stringWithFormat:@"I-NET%06ld", (long)messageCode], formattedMessage,
+ NULL);
+ }
+}
+
+- (void)firNetwork_logWithLevel:(FIRNetworkLogLevel)logLevel
+ messageCode:(FIRNetworkMessageCode)messageCode
+ message:(NSString *)message
+ context:(id)context {
+ if (_loggerDelegate) {
+ [_loggerDelegate firNetwork_logWithLevel:logLevel
+ messageCode:messageCode
+ message:message
+ context:context];
+ return;
+ }
+ NSArray *contexts = context ? @[ context ] : @[];
+ [self firNetwork_logWithLevel:logLevel messageCode:messageCode message:message contexts:contexts];
+}
+
+- (void)firNetwork_logWithLevel:(FIRNetworkLogLevel)logLevel
+ messageCode:(FIRNetworkMessageCode)messageCode
+ message:(NSString *)message {
+ if (_loggerDelegate) {
+ [_loggerDelegate firNetwork_logWithLevel:logLevel messageCode:messageCode message:message];
+ return;
+ }
+ [self firNetwork_logWithLevel:logLevel messageCode:messageCode message:message contexts:@[]];
+}
+
+/// Returns a string for the given log level (e.g. kFIRNetworkLogLevelError -> @"ERROR").
+static NSString *FIRLogLevelDescriptionFromLogLevel(FIRNetworkLogLevel logLevel) {
+ static NSDictionary *levelNames = nil;
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ levelNames = @{
+ @(kFIRNetworkLogLevelError) : @"ERROR",
+ @(kFIRNetworkLogLevelWarning) : @"WARNING",
+ @(kFIRNetworkLogLevelInfo) : @"INFO",
+ @(kFIRNetworkLogLevelDebug) : @"DEBUG"
+ };
+ });
+ return levelNames[@(logLevel)];
+}
+
+/// Returns a formatted string to be used for console logging.
+static NSString *FIRStringWithLogMessage(NSString *message, FIRNetworkLogLevel logLevel,
+ NSArray *contexts) {
+ if (!message) {
+ message = @"(Message was nil)";
+ } else if (!message.length) {
+ message = @"(Message was empty)";
+ }
+ NSMutableString *result = [[NSMutableString alloc]
+ initWithFormat:@"<%@/%@> %@", kFIRNetworkLogTag, FIRLogLevelDescriptionFromLogLevel(logLevel),
+ message];
+
+ if (!contexts.count) {
+ return result;
+ }
+
+ NSMutableArray *formattedContexts = [[NSMutableArray alloc] init];
+ for (id item in contexts) {
+ [formattedContexts addObject:(item != [NSNull null] ? item : @"(nil)")];
+ }
+
+ [result appendString:@": "];
+ [result appendString:[formattedContexts componentsJoinedByString:@", "]];
+ return result;
+}
+
+@end
diff --git a/Firebase/Core/FIRNetworkConstants.m b/Firebase/Core/FIRNetworkConstants.m
new file mode 100644
index 0000000..7ba0e15
--- /dev/null
+++ b/Firebase/Core/FIRNetworkConstants.m
@@ -0,0 +1,39 @@
+// Copyright 2017 Google
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "Private/FIRNetworkConstants.h"
+
+@import Foundation;
+
+NSString *const kFIRNetworkBackgroundSessionConfigIDPrefix =
+ @"com.firebase.network.background-upload";
+NSString *const kFIRNetworkApplicationSupportSubdirectory = @"Firebase/Network";
+NSString *const kFIRNetworkTempDirectoryName = @"FIRNetworkTemporaryDirectory";
+const NSTimeInterval kFIRNetworkTempFolderExpireTime = 60 * 60; // 1 hour
+const NSTimeInterval kFIRNetworkTimeOutInterval = 60; // 1 minute.
+NSString *const kFIRNetworkReachabilityHost = @"app-measurement.com";
+NSString *const kFIRNetworkErrorContext = @"Context";
+
+const int kFIRNetworkHTTPStatusOK = 200;
+const int kFIRNetworkHTTPStatusNoContent = 204;
+const int kFIRNetworkHTTPStatusCodeMultipleChoices = 300;
+const int kFIRNetworkHTTPStatusCodeMovedPermanently = 301;
+const int kFIRNetworkHTTPStatusCodeFound = 302;
+const int kFIRNetworkHTTPStatusCodeNotModified = 304;
+const int kFIRNetworkHTTPStatusCodeMovedTemporarily = 307;
+const int kFIRNetworkHTTPStatusCodeNotFound = 404;
+const int kFIRNetworkHTTPStatusCodeCannotAcceptTraffic = 429;
+const int kFIRNetworkHTTPStatusCodeUnavailable = 503;
+
+NSString *const kFIRNetworkErrorDomain = @"com.firebase.network.ErrorDomain";
diff --git a/Firebase/Core/FIRNetworkURLSession.m b/Firebase/Core/FIRNetworkURLSession.m
new file mode 100644
index 0000000..2b17eb3
--- /dev/null
+++ b/Firebase/Core/FIRNetworkURLSession.m
@@ -0,0 +1,669 @@
+// Copyright 2017 Google
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "Private/FIRNetworkURLSession.h"
+
+#import "Private/FIRMutableDictionary.h"
+#import "Private/FIRNetworkConstants.h"
+#import "Private/FIRNetworkMessageCode.h"
+#import "Private/FIRLogger.h"
+
+@implementation FIRNetworkURLSession {
+ /// The handler to be called when the request completes or error has occurs.
+ FIRNetworkURLSessionCompletionHandler _completionHandler;
+
+ /// Session ID generated randomly with a fixed prefix.
+ NSString *_sessionID;
+
+ /// The session configuration.
+ NSURLSessionConfiguration *_sessionConfig;
+
+ /// The path to the directory where all temporary files are stored before uploading.
+ NSURL *_networkDirectoryURL;
+
+ /// The downloaded data from fetching.
+ NSData *_downloadedData;
+
+ /// The path to the temporary file which stores the uploading data.
+ NSURL *_uploadingFileURL;
+
+ /// The current request.
+ NSURLRequest *_request;
+}
+
+#pragma mark - Init
+
+- (instancetype)initWithNetworkLoggerDelegate:(id<FIRNetworkLoggerDelegate>)networkLoggerDelegate {
+ self = [super init];
+ if (self) {
+ // Create URL to the directory where all temporary files to upload have to be stored.
+ NSArray *paths =
+ NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES);
+ NSString *applicationSupportDirectory = paths.firstObject;
+ NSArray *tempPathComponents = @[
+ applicationSupportDirectory,
+ kFIRNetworkApplicationSupportSubdirectory,
+ kFIRNetworkTempDirectoryName
+ ];
+ _networkDirectoryURL = [NSURL fileURLWithPathComponents:tempPathComponents];
+ _sessionID = [NSString stringWithFormat:@"%@-%@", kFIRNetworkBackgroundSessionConfigIDPrefix,
+ [[NSUUID UUID] UUIDString]];
+ _loggerDelegate = networkLoggerDelegate;
+ }
+ return self;
+}
+
+#pragma mark - External Methods
+
+#pragma mark - To be called from AppDelegate
+
++ (void)handleEventsForBackgroundURLSessionID:(NSString *)sessionID
+ completionHandler:
+ (FIRNetworkSystemCompletionHandler)systemCompletionHandler {
+ // The session may not be FIRAnalytics background. Ignore those that do not have the prefix.
+ if (![sessionID hasPrefix:kFIRNetworkBackgroundSessionConfigIDPrefix]) {
+ return;
+ }
+ FIRNetworkURLSession *fetcher = [self fetcherWithSessionIdentifier:sessionID];
+ if (fetcher != nil) {
+ [fetcher addSystemCompletionHandler:systemCompletionHandler forSession:sessionID];
+ } else {
+ FIRLogError(kFIRLoggerCore,
+ [NSString stringWithFormat:@"I-NET%06ld", (long)kFIRNetworkMessageCodeNetwork003],
+ @"Failed to retrieve background session with ID %@ after app is relaunched.",
+ sessionID);
+ }
+}
+
+#pragma mark - External Methods
+
+/// Sends an async POST request using NSURLSession for iOS >= 7.0, and returns an ID of the
+/// connection.
+- (NSString *)sessionIDFromAsyncPOSTRequest:(NSURLRequest *)request
+ completionHandler:(FIRNetworkURLSessionCompletionHandler)handler {
+ // NSURLSessionUploadTask does not work with NSData in the background.
+ // To avoid this issue, write the data to a temporary file to upload it.
+ // Make a temporary file with the data subset.
+ _uploadingFileURL = [self temporaryFilePathWithSessionID:_sessionID];
+ NSError *writeError;
+ NSURLSessionUploadTask *postRequestTask;
+ NSURLSession *session;
+ BOOL didWriteFile = NO;
+
+ // Clean up the entire temp folder to avoid temp files that remain in case the previous session
+ // crashed and did not clean up.
+ [self maybeRemoveTempFilesAtURL:_networkDirectoryURL
+ expiringTime:kFIRNetworkTempFolderExpireTime];
+
+ // If there is no background network enabled, no need to write to file. This will allow default
+ // network session which runs on the foreground.
+ if (_backgroundNetworkEnabled && [self ensureTemporaryDirectoryExists]) {
+ didWriteFile = [request.HTTPBody writeToFile:_uploadingFileURL.path
+ options:NSDataWritingAtomic
+ error:&writeError];
+
+ if (writeError) {
+ [_loggerDelegate firNetwork_logWithLevel:kFIRNetworkLogLevelError
+ messageCode:kFIRNetworkMessageCodeURLSession000
+ message:@"Failed to write request data to file"
+ context:writeError];
+ }
+ }
+
+ if (didWriteFile) {
+ // Exclude this file from backing up to iTunes. There are conflicting reports that excluding
+ // directory from backing up does not excluding files of that directory from backing up.
+ [self excludeFromBackupForURL:_uploadingFileURL];
+
+ _sessionConfig = [self backgroundSessionConfigWithSessionID:_sessionID];
+ [self populateSessionConfig:_sessionConfig withRequest:request];
+ session = [NSURLSession sessionWithConfiguration:_sessionConfig
+ delegate:self
+ delegateQueue:[NSOperationQueue mainQueue]];
+ postRequestTask = [session uploadTaskWithRequest:request fromFile:_uploadingFileURL];
+ } else {
+ // If we cannot write to file, just send it in the foreground.
+ _sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
+ [self populateSessionConfig:_sessionConfig withRequest:request];
+ _sessionConfig.URLCache = nil;
+ session = [NSURLSession sessionWithConfiguration:_sessionConfig
+ delegate:self
+ delegateQueue:[NSOperationQueue mainQueue]];
+ postRequestTask = [session uploadTaskWithRequest:request fromData:request.HTTPBody];
+ }
+
+ if (!session || !postRequestTask) {
+ NSError *error =
+ [[NSError alloc] initWithDomain:kFIRNetworkErrorDomain
+ code:FIRErrorCodeNetworkRequestCreation
+ userInfo:@{
+ kFIRNetworkErrorContext : @"Cannot create network session"
+ }];
+ [self callCompletionHandler:handler withResponse:nil data:nil error:error];
+ return nil;
+ }
+
+ // Save the session into memory.
+ NSMapTable *sessionIdentifierToFetcherMap = [[self class] sessionIDToFetcherMap];
+ [sessionIdentifierToFetcherMap setObject:self forKey:_sessionID];
+
+ _request = [request copy];
+
+ // Store completion handler because background session does not accept handler block but custom
+ // delegate.
+ _completionHandler = [handler copy];
+ [postRequestTask resume];
+
+ return _sessionID;
+}
+
+/// Sends an async GET request using NSURLSession for iOS >= 7.0, and returns an ID of the session.
+- (NSString *)sessionIDFromAsyncGETRequest:(NSURLRequest *)request
+ completionHandler:(FIRNetworkURLSessionCompletionHandler)handler {
+ if (_backgroundNetworkEnabled) {
+ _sessionConfig = [self backgroundSessionConfigWithSessionID:_sessionID];
+ } else {
+ _sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
+ }
+
+ [self populateSessionConfig:_sessionConfig withRequest:request];
+
+ // Do not cache the GET request.
+ _sessionConfig.URLCache = nil;
+
+ NSURLSession *session = [NSURLSession sessionWithConfiguration:_sessionConfig
+ delegate:self
+ delegateQueue:[NSOperationQueue mainQueue]];
+ NSURLSessionDownloadTask *downloadTask = [session downloadTaskWithRequest:request];
+
+ if (!session || !downloadTask) {
+ NSError *error =
+ [[NSError alloc] initWithDomain:kFIRNetworkErrorDomain
+ code:FIRErrorCodeNetworkRequestCreation
+ userInfo:@{
+ kFIRNetworkErrorContext : @"Cannot create network session"
+ }];
+ [self callCompletionHandler:handler withResponse:nil data:nil error:error];
+ return nil;
+ }
+
+ // Save the session into memory.
+ NSMapTable *sessionIdentifierToFetcherMap = [[self class] sessionIDToFetcherMap];
+ [sessionIdentifierToFetcherMap setObject:self forKey:_sessionID];
+
+ _request = [request copy];
+
+ _completionHandler = [handler copy];
+ [downloadTask resume];
+
+ return _sessionID;
+}
+
+#pragma mark - NSURLSessionTaskDelegate
+
+/// Called by the NSURLSession once the download task is completed. The file is saved in the
+/// provided URL so we need to read the data and store into _downloadedData. Once the session is
+/// completed, URLSession:task:didCompleteWithError will be called and the completion handler will
+/// be called with the downloaded data.
+- (void)URLSession:(NSURLSession *)session
+ downloadTask:(NSURLSessionDownloadTask *)task
+ didFinishDownloadingToURL:(NSURL *)url {
+ if (!url.path) {
+ [_loggerDelegate
+ firNetwork_logWithLevel:kFIRNetworkLogLevelError
+ messageCode:kFIRNetworkMessageCodeURLSession001
+ message:@"Unable to read downloaded data from empty temp path"];
+ _downloadedData = nil;
+ return;
+ }
+
+ NSError *error;
+ _downloadedData = [NSData dataWithContentsOfFile:url.path options:0 error:&error];
+
+ if (error) {
+ [_loggerDelegate firNetwork_logWithLevel:kFIRNetworkLogLevelError
+ messageCode:kFIRNetworkMessageCodeURLSession002
+ message:@"Cannot read the content of downloaded data"
+ context:error];
+ _downloadedData = nil;
+ }
+}
+
+- (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session {
+ [_loggerDelegate firNetwork_logWithLevel:kFIRNetworkLogLevelDebug
+ messageCode:kFIRNetworkMessageCodeURLSession003
+ message:@"Background session finished"
+ context:session.configuration.identifier];
+ [self callSystemCompletionHandler:session.configuration.identifier];
+}
+
+- (void)URLSession:(NSURLSession *)session
+ task:(NSURLSessionTask *)task
+ didCompleteWithError:(NSError *)error {
+ // Avoid any chance of recursive behavior leading to it being used repeatedly.
+ FIRNetworkURLSessionCompletionHandler handler = _completionHandler;
+ _completionHandler = nil;
+
+ if (task.response) {
+ // The following assertion should always be true for HTTP requests, see https://goo.gl/gVLxT7.
+ NSAssert([task.response isKindOfClass:[NSHTTPURLResponse class]], @"URL response must be HTTP");
+
+ // The server responded so ignore the error created by the system.
+ error = nil;
+ } else if (!error) {
+ error =
+ [[NSError alloc] initWithDomain:kFIRNetworkErrorDomain
+ code:FIRErrorCodeNetworkInvalidResponse
+ userInfo:@{
+ kFIRNetworkErrorContext : @"Network Error: Empty network response"
+ }];
+ }
+
+ [self callCompletionHandler:handler
+ withResponse:(NSHTTPURLResponse *)task.response
+ data:_downloadedData
+ error:error];
+
+ // Remove the temp file to avoid trashing devices with lots of temp files.
+ [self removeTempItemAtURL:_uploadingFileURL];
+
+ // Try to clean up stale files again.
+ [self maybeRemoveTempFilesAtURL:_networkDirectoryURL
+ expiringTime:kFIRNetworkTempFolderExpireTime];
+}
+
+- (void)URLSession:(NSURLSession *)session
+ task:(NSURLSessionTask *)task
+ didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
+ completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition,
+ NSURLCredential *credential))completionHandler {
+ // The handling is modeled after GTMSessionFetcher.
+ if ([challenge.protectionSpace.authenticationMethod
+ isEqualToString:NSURLAuthenticationMethodServerTrust]) {
+ SecTrustRef serverTrust = challenge.protectionSpace.serverTrust;
+ if (serverTrust == NULL) {
+ [_loggerDelegate firNetwork_logWithLevel:kFIRNetworkLogLevelDebug
+ messageCode:kFIRNetworkMessageCodeURLSession004
+ message:@"Received empty server trust for host. Host"
+ context:_request.URL];
+ completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil);
+ return;
+ }
+ NSURLCredential *credential = [NSURLCredential credentialForTrust:serverTrust];
+ if (!credential) {
+ [_loggerDelegate firNetwork_logWithLevel:kFIRNetworkLogLevelWarning
+ messageCode:kFIRNetworkMessageCodeURLSession005
+ message:@"Unable to verify server identity. Host"
+ context:_request.URL];
+ completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);
+ return;
+ }
+
+ [_loggerDelegate firNetwork_logWithLevel:kFIRNetworkLogLevelDebug
+ messageCode:kFIRNetworkMessageCodeURLSession006
+ message:@"Received SSL challenge for host. Host"
+ context:_request.URL];
+
+ void (^callback)(BOOL) = ^(BOOL allow) {
+ if (allow) {
+ completionHandler(NSURLSessionAuthChallengeUseCredential, credential);
+ } else {
+ [_loggerDelegate
+ firNetwork_logWithLevel:kFIRNetworkLogLevelDebug
+ messageCode:kFIRNetworkMessageCodeURLSession007
+ message:@"Cancelling authentication challenge for host. Host"
+ context:_request.URL];
+ completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);
+ }
+ };
+
+ // Retain the trust object to avoid a SecTrustEvaluate() crash on iOS 7.
+ CFRetain(serverTrust);
+
+ // Evaluate the certificate chain.
+ //
+ // The delegate queue may be the main thread. Trust evaluation could cause some
+ // blocking network activity, so we must evaluate async, as documented at
+ // https://developer.apple.com/library/ios/technotes/tn2232/
+ dispatch_queue_t evaluateBackgroundQueue =
+ dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
+
+ dispatch_async(evaluateBackgroundQueue, ^{
+ SecTrustResultType trustEval = kSecTrustResultInvalid;
+ BOOL shouldAllow;
+ OSStatus trustError;
+
+ @synchronized([FIRNetworkURLSession class]) {
+ trustError = SecTrustEvaluate(serverTrust, &trustEval);
+ }
+
+ if (trustError != errSecSuccess) {
+ [_loggerDelegate firNetwork_logWithLevel:kFIRNetworkLogLevelError
+ messageCode:kFIRNetworkMessageCodeURLSession008
+ message:@"Cannot evaluate server trust. Error, host"
+ contexts:@[ @(trustError), _request.URL ]];
+ shouldAllow = NO;
+ } else {
+ // Having a trust level "unspecified" by the user is the usual result, described at
+ // https://developer.apple.com/library/mac/qa/qa1360
+ shouldAllow =
+ (trustEval == kSecTrustResultUnspecified || trustEval == kSecTrustResultProceed);
+ }
+
+ // Call the call back with the permission.
+ callback(shouldAllow);
+
+ CFRelease(serverTrust);
+ });
+ return;
+ }
+
+ // Default handling for other Auth Challenges.
+ completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil);
+}
+
+#pragma mark - Internal Methods
+
+/// Stores system completion handler with session ID as key.
+- (void)addSystemCompletionHandler:(FIRNetworkSystemCompletionHandler)handler
+ forSession:(NSString *)identifier {
+ if (!handler) {
+ [_loggerDelegate
+ firNetwork_logWithLevel:kFIRNetworkLogLevelError
+ messageCode:kFIRNetworkMessageCodeURLSession009
+ message:@"Cannot store nil system completion handler in network"];
+ return;
+ }
+
+ if (!identifier.length) {
+ [_loggerDelegate
+ firNetwork_logWithLevel:kFIRNetworkLogLevelError
+ messageCode:kFIRNetworkMessageCodeURLSession010
+ message:@"Cannot store system completion handler with empty network "
+ "session identifier"];
+ return;
+ }
+
+ FIRMutableDictionary *systemCompletionHandlers =
+ [[self class] sessionIDToSystemCompletionHandlerDictionary];
+ if (systemCompletionHandlers[identifier]) {
+ [_loggerDelegate firNetwork_logWithLevel:kFIRNetworkLogLevelWarning
+ messageCode:kFIRNetworkMessageCodeURLSession011
+ message:@"Got multiple system handlers for a single session ID"
+ context:identifier];
+ }
+
+ systemCompletionHandlers[identifier] = handler;
+}
+
+/// Calls the system provided completion handler with the session ID stored in the dictionary.
+/// The handler will be removed from the dictionary after being called.
+- (void)callSystemCompletionHandler:(NSString *)identifier {
+ FIRMutableDictionary *systemCompletionHandlers =
+ [[self class] sessionIDToSystemCompletionHandlerDictionary];
+ FIRNetworkSystemCompletionHandler handler = [systemCompletionHandlers objectForKey:identifier];
+
+ if (handler) {
+ [systemCompletionHandlers removeObjectForKey:identifier];
+
+ dispatch_async(dispatch_get_main_queue(), ^{
+ handler();
+ });
+ }
+}
+
+/// Sets or updates the session ID of this session.
+- (void)setSessionID:(NSString *)sessionID {
+ _sessionID = [sessionID copy];
+}
+
+/// Creates a background session configuration with the session ID using the supported method.
+- (NSURLSessionConfiguration *)backgroundSessionConfigWithSessionID:(NSString *)sessionID {
+#if (!TARGET_OS_IPHONE && defined(MAC_OS_X_VERSION_10_10) && \
+ MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_10) || \
+ (TARGET_OS_IPHONE && defined(__IPHONE_8_0) && \
+ __IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_8_0)
+ // iOS 8/10.10 builds require the new backgroundSessionConfiguration method name.
+ return [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:sessionID];
+#elif (!TARGET_OS_IPHONE && defined(MAC_OS_X_VERSION_10_10) && \
+ MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_10) || \
+ (TARGET_OS_IPHONE && defined(__IPHONE_8_0) && __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_8_0)
+ // Do a runtime check to avoid a deprecation warning about using
+ // +backgroundSessionConfiguration: on iOS 8.
+ if ([NSURLSessionConfiguration
+ respondsToSelector:@selector(backgroundSessionConfigurationWithIdentifier:)]) {
+ // Running on iOS 8+/OS X 10.10+.
+ return [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:sessionID];
+ } else {
+ // Running on iOS 7/OS X 10.9.
+ return [NSURLSessionConfiguration backgroundSessionConfiguration:sessionID];
+ }
+#else
+ // Building with an SDK earlier than iOS 8/OS X 10.10.
+ return [NSURLSessionConfiguration backgroundSessionConfiguration:sessionID];
+#endif
+}
+
+- (void)maybeRemoveTempFilesAtURL:(NSURL *)folderURL expiringTime:(NSTimeInterval)staleTime {
+ if (!folderURL.absoluteString.length) {
+ return;
+ }
+
+ NSFileManager *fileManager = [NSFileManager defaultManager];
+ NSError *error = nil;
+
+ NSArray *properties = @[ NSURLCreationDateKey ];
+ NSArray *directoryContent =
+ [fileManager contentsOfDirectoryAtURL:folderURL
+ includingPropertiesForKeys:properties
+ options:NSDirectoryEnumerationSkipsSubdirectoryDescendants
+ error:&error];
+ if (error && error.code != NSFileReadNoSuchFileError) {
+ [_loggerDelegate
+ firNetwork_logWithLevel:kFIRNetworkLogLevelDebug
+ messageCode:kFIRNetworkMessageCodeURLSession012
+ message:@"Cannot get files from the temporary network folder. Error"
+ context:error];
+ return;
+ }
+
+ if (!directoryContent.count) {
+ return;
+ }
+
+ NSTimeInterval now = [NSDate date].timeIntervalSince1970;
+ for (NSURL *tempFile in directoryContent) {
+ NSDate *creationDate;
+ BOOL getCreationDate =
+ [tempFile getResourceValue:&creationDate forKey:NSURLCreationDateKey error:NULL];
+ if (!getCreationDate) {
+ continue;
+ }
+ NSTimeInterval creationTimeInterval = creationDate.timeIntervalSince1970;
+ if (fabs(now - creationTimeInterval) > staleTime) {
+ [self removeTempItemAtURL:tempFile];
+ }
+ }
+}
+
+/// Removes the temporary file written to disk for sending the request. It has to be cleaned up
+/// after the session is done.
+- (void)removeTempItemAtURL:(NSURL *)fileURL {
+ if (!fileURL.absoluteString.length) {
+ return;
+ }
+
+ NSFileManager *fileManager = [NSFileManager defaultManager];
+ NSError *error = nil;
+
+ if (![fileManager removeItemAtURL:fileURL error:&error] && error.code != NSFileNoSuchFileError) {
+ [_loggerDelegate
+ firNetwork_logWithLevel:kFIRNetworkLogLevelError
+ messageCode:kFIRNetworkMessageCodeURLSession013
+ message:@"Failed to remove temporary uploading data file. Error"
+ context:error.localizedDescription];
+ }
+}
+
+/// Gets the fetcher with the session ID.
++ (instancetype)fetcherWithSessionIdentifier:(NSString *)sessionIdentifier {
+ NSMapTable *sessionIdentifierToFetcherMap = [self sessionIDToFetcherMap];
+ FIRNetworkURLSession *session = [sessionIdentifierToFetcherMap objectForKey:sessionIdentifier];
+ if (!session && [sessionIdentifier hasPrefix:kFIRNetworkBackgroundSessionConfigIDPrefix]) {
+ session = [[FIRNetworkURLSession alloc] initWithNetworkLoggerDelegate:nil];
+ [session setSessionID:sessionIdentifier];
+ [sessionIdentifierToFetcherMap setObject:session forKey:sessionIdentifier];
+ }
+ return session;
+}
+
+/// Returns a map of the fetcher by session ID. Creates a map if it is not created.
++ (NSMapTable *)sessionIDToFetcherMap {
+ static NSMapTable *sessionIDToFetcherMap;
+
+ static dispatch_once_t sessionMapOnceToken;
+ dispatch_once(&sessionMapOnceToken, ^{
+ sessionIDToFetcherMap = [NSMapTable strongToWeakObjectsMapTable];
+ });
+ return sessionIDToFetcherMap;
+}
+
+/// Returns a map of system provided completion handler by session ID. Creates a map if it is not
+/// created.
++ (FIRMutableDictionary *)sessionIDToSystemCompletionHandlerDictionary {
+ static FIRMutableDictionary *systemCompletionHandlers;
+
+ static dispatch_once_t systemCompletionHandlerOnceToken;
+ dispatch_once(&systemCompletionHandlerOnceToken, ^{
+ systemCompletionHandlers = [[FIRMutableDictionary alloc] init];
+ });
+ return systemCompletionHandlers;
+}
+
+- (NSURL *)temporaryFilePathWithSessionID:(NSString *)sessionID {
+ NSString *tempName = [NSString stringWithFormat:@"FIRUpload_temp_%@", sessionID];
+ return [_networkDirectoryURL URLByAppendingPathComponent:tempName];
+}
+
+/// Makes sure that the directory to store temp files exists. If not, tries to create it and returns
+/// YES. If there is anything wrong, returns NO.
+- (BOOL)ensureTemporaryDirectoryExists {
+ NSFileManager *fileManager = [NSFileManager defaultManager];
+ NSError *error = nil;
+
+ // Create a temporary directory if it does not exist or was deleted.
+ if ([_networkDirectoryURL checkResourceIsReachableAndReturnError:&error]) {
+ return YES;
+ }
+
+ if (error && error.code != NSFileReadNoSuchFileError) {
+ [_loggerDelegate
+ firNetwork_logWithLevel:kFIRNetworkLogLevelWarning
+ messageCode:kFIRNetworkMessageCodeURLSession014
+ message:@"Error while trying to access Network temp folder. Error"
+ context:error];
+ }
+
+ NSError *writeError = nil;
+
+ [fileManager createDirectoryAtURL:_networkDirectoryURL
+ withIntermediateDirectories:YES
+ attributes:nil
+ error:&writeError];
+ if (writeError) {
+ [_loggerDelegate firNetwork_logWithLevel:kFIRNetworkLogLevelError
+ messageCode:kFIRNetworkMessageCodeURLSession015
+ message:@"Cannot create temporary directory. Error"
+ context:writeError];
+ return NO;
+ }
+
+ // Set the iCloud exclusion attribute on the Documents URL.
+ [self excludeFromBackupForURL:_networkDirectoryURL];
+
+ return YES;
+}
+
+- (void)excludeFromBackupForURL:(NSURL *)url {
+ if (!url.path) {
+ return;
+ }
+
+ // Set the iCloud exclusion attribute on the Documents URL.
+ NSError *preventBackupError = nil;
+ [url setResourceValue:@YES forKey:NSURLIsExcludedFromBackupKey error:&preventBackupError];
+ if (preventBackupError) {
+ [_loggerDelegate firNetwork_logWithLevel:kFIRNetworkLogLevelError
+ messageCode:kFIRNetworkMessageCodeURLSession016
+ message:@"Cannot exclude temporary folder from iTunes backup"];
+ }
+}
+
+- (void)URLSession:(NSURLSession *)session
+ task:(NSURLSessionTask *)task
+ willPerformHTTPRedirection:(NSHTTPURLResponse *)response
+ newRequest:(NSURLRequest *)request
+ completionHandler:(void (^)(NSURLRequest *))completionHandler {
+ NSArray *nonAllowedRedirectionCodes = @[
+ @(kFIRNetworkHTTPStatusCodeFound),
+ @(kFIRNetworkHTTPStatusCodeMovedPermanently),
+ @(kFIRNetworkHTTPStatusCodeMovedTemporarily),
+ @(kFIRNetworkHTTPStatusCodeMultipleChoices)
+ ];
+
+ // Allow those not in the non allowed list to be followed.
+ if (![nonAllowedRedirectionCodes containsObject:@(response.statusCode)]) {
+ completionHandler(request);
+ return;
+ }
+
+ // Do not allow redirection if the response code is in the non-allowed list.
+ NSURLRequest *newRequest = request;
+
+ if (response) {
+ newRequest = nil;
+ }
+
+ completionHandler(newRequest);
+}
+
+#pragma mark - Helper Methods
+
+- (void)callCompletionHandler:(FIRNetworkURLSessionCompletionHandler)handler
+ withResponse:(NSHTTPURLResponse *)response
+ data:(NSData *)data
+ error:(NSError *)error {
+ if (error) {
+ [_loggerDelegate firNetwork_logWithLevel:kFIRNetworkLogLevelError
+ messageCode:kFIRNetworkMessageCodeURLSession017
+ message:@"Encounter network error. Code, error"
+ contexts:@[@(error.code), error]];
+ }
+
+ if (handler) {
+ dispatch_async(dispatch_get_main_queue(), ^{
+ handler(response, data, _sessionID, error);
+ });
+ }
+}
+
+- (void)populateSessionConfig:(NSURLSessionConfiguration *)sessionConfig
+ withRequest:(NSURLRequest *)request {
+ sessionConfig.HTTPAdditionalHeaders = request.allHTTPHeaderFields;
+ sessionConfig.timeoutIntervalForRequest = request.timeoutInterval;
+ sessionConfig.timeoutIntervalForResource = request.timeoutInterval;
+ sessionConfig.requestCachePolicy = request.cachePolicy;
+}
+
+@end
diff --git a/Firebase/Core/FIROptions.h b/Firebase/Core/FIROptions.h
new file mode 100644
index 0000000..5bae59c
--- /dev/null
+++ b/Firebase/Core/FIROptions.h
@@ -0,0 +1,131 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+#import "FIRCoreSwiftNameSupport.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * This class provides constant fields of Google APIs.
+ */
+FIR_SWIFT_NAME(FirebaseOptions)
+@interface FIROptions : NSObject<NSCopying>
+
+/**
+ * Returns the default options.
+ */
++ (nullable FIROptions *)defaultOptions FIR_SWIFT_NAME(defaultOptions());
+
+/**
+ * An iOS API key used for authenticating requests from your app, e.g.
+ * @"AIzaSyDdVgKwhZl0sTTTLZ7iTmt1r3N2cJLnaDk", used to identify your app to Google servers.
+ */
+@property(nonatomic, copy, nullable) NSString *APIKey FIR_SWIFT_NAME(apiKey);
+
+/**
+ * The bundle ID for the application. Defaults to `[[NSBundle mainBundle] bundleID]` when not set
+ * manually or in a plist.
+ */
+@property(nonatomic, copy) NSString *bundleID;
+
+/**
+ * The OAuth2 client ID for iOS application used to authenticate Google users, for example
+ * @"12345.apps.googleusercontent.com", used for signing in with Google.
+ */
+@property(nonatomic, copy, nullable) NSString *clientID;
+
+/**
+ * The tracking ID for Google Analytics, e.g. @"UA-12345678-1", used to configure Google Analytics.
+ */
+@property(nonatomic, copy, nullable) NSString *trackingID;
+
+/**
+ * The Project Number from the Google Developer's console, for example @"012345678901", used to
+ * configure Google Cloud Messaging.
+ */
+@property(nonatomic, copy) NSString *GCMSenderID FIR_SWIFT_NAME(gcmSenderID);
+
+/**
+ * The Project ID from the Firebase console, for example @"abc-xyz-123".
+ */
+@property(nonatomic, copy, nullable) NSString *projectID;
+
+/**
+ * The Android client ID used in Google AppInvite when an iOS app has its Android version, for
+ * example @"12345.apps.googleusercontent.com".
+ */
+@property(nonatomic, copy, nullable) NSString *androidClientID;
+
+/**
+ * The Google App ID that is used to uniquely identify an instance of an app.
+ */
+@property(nonatomic, copy) NSString *googleAppID;
+
+/**
+ * The database root URL, e.g. @"http://abc-xyz-123.firebaseio.com".
+ */
+@property(nonatomic, copy, nullable) NSString *databaseURL;
+
+/**
+ * The URL scheme used to set up Durable Deep Link service.
+ */
+@property(nonatomic, copy, nullable) NSString *deepLinkURLScheme;
+
+/**
+ * The Google Cloud Storage bucket name, e.g. @"abc-xyz-123.storage.firebase.com".
+ */
+@property(nonatomic, copy, nullable) NSString *storageBucket;
+
+/**
+ * Initializes a customized instance of FIROptions with keys. googleAppID, bundleID and GCMSenderID
+ * are required. Other keys may required for configuring specific services.
+ */
+- (instancetype)initWithGoogleAppID:(NSString *)googleAppID
+ bundleID:(NSString *)bundleID
+ GCMSenderID:(NSString *)GCMSenderID
+ APIKey:(NSString *)APIKey
+ clientID:(NSString *)clientID
+ trackingID:(NSString *)trackingID
+ androidClientID:(NSString *)androidClientID
+ databaseURL:(NSString *)databaseURL
+ storageBucket:(NSString *)storageBucket
+ deepLinkURLScheme:(NSString *)deepLinkURLScheme
+ DEPRECATED_MSG_ATTRIBUTE("Use `-[FIROptions initWithGoogleAppID:gcmSenderID:]` and "
+ "properties instead.");
+
+/**
+ * Initializes a customized instance of FIROptions from the file at the given plist file path.
+ * For example,
+ * NSString *filePath =
+ * [[NSBundle mainBundle] pathForResource:@"GoogleService-Info" ofType:@"plist"];
+ * FIROptions *options = [[FIROptions alloc] initWithContentsOfFile:filePath];
+ * Returns nil if the plist file does not exist or is invalid.
+ */
+- (nullable instancetype)initWithContentsOfFile:(NSString *)plistPath;
+
+/**
+ * Initializes a customized instance of FIROptions with required fields. Use the mutable properties
+ * to modify fields for configuring specific services.
+ */
+- (instancetype)initWithGoogleAppID:(NSString *)googleAppID
+ GCMSenderID:(NSString *)GCMSenderID
+ FIR_SWIFT_NAME(init(googleAppID:gcmSenderID:));
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Core/FIROptions.m b/Firebase/Core/FIROptions.m
new file mode 100644
index 0000000..6e19c82
--- /dev/null
+++ b/Firebase/Core/FIROptions.m
@@ -0,0 +1,427 @@
+// Copyright 2017 Google
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "Private/FIRBundleUtil.h"
+#import "Private/FIRErrors.h"
+#import "Private/FIRLogger.h"
+#import "Private/FIROptionsInternal.h"
+
+// Keys for the strings in the plist file.
+NSString *const kFIRAPIKey = @"API_KEY";
+NSString *const kFIRTrackingID = @"TRACKING_ID";
+NSString *const kFIRGoogleAppID = @"GOOGLE_APP_ID";
+NSString *const kFIRClientID = @"CLIENT_ID";
+NSString *const kFIRGCMSenderID = @"GCM_SENDER_ID";
+NSString *const kFIRAndroidClientID = @"ANDROID_CLIENT_ID";
+NSString *const kFIRDatabaseURL = @"DATABASE_URL";
+NSString *const kFIRStorageBucket = @"STORAGE_BUCKET";
+// The key to locate the expected bundle identifier in the plist file.
+NSString *const kFIRBundleID = @"BUNDLE_ID";
+// The key to locate the project identifier in the plist file.
+NSString *const kFIRProjectID = @"PROJECT_ID";
+
+NSString *const kFIRIsMeasurementEnabled = @"IS_MEASUREMENT_ENABLED";
+NSString *const kFIRIsAnalyticsCollectionEnabled = @"FIREBASE_ANALYTICS_COLLECTION_ENABLED";
+NSString *const kFIRIsAnalyticsCollectionDeactivated = @"FIREBASE_ANALYTICS_COLLECTION_DEACTIVATED";
+
+NSString *const kFIRIsAnalyticsEnabled = @"IS_ANALYTICS_ENABLED";
+NSString *const kFIRIsSignInEnabled = @"IS_SIGNIN_ENABLED";
+
+// Library version ID.
+NSString *const kFIRLibraryVersionID =
+ @"4" // Major version (one or more digits)
+ @"00" // Minor version (exactly 2 digits)
+ @"00" // Build number (exactly 2 digits)
+ @"000"; // Fixed "000"
+// Plist file name.
+NSString *const kServiceInfoFileName = @"GoogleService-Info";
+// Plist file type.
+NSString *const kServiceInfoFileType = @"plist";
+
+// Exception raised from attempting to modify a FIROptions after it's been copied to a FIRApp.
+NSString *const kFIRExceptionBadModification =
+ @"Attempted to modify options after it's set on FIRApp. Please modify all properties before "
+ @"initializing FIRApp.";
+
+@interface FIROptions ()
+
+/**
+ * This property maintains the actual configuration key-value pairs.
+ */
+@property(nonatomic, readwrite) NSMutableDictionary *optionsDictionary;
+
+/**
+ * Combination of analytics options from both the main plist and the GoogleService-info.plist.
+ * Values which are present in the main plist override values from the GoogleService-info.plist.
+ */
+@property(nonatomic, readonly) NSDictionary *analyticsOptionsDictionary;
+
+@end
+
+@implementation FIROptions {
+ /// Backing variable for self.analyticsOptionsDictionary.
+ NSDictionary *_analyticsOptionsDictionary;
+ dispatch_once_t _createAnalyticsOptionsDictionaryOnce;
+}
+
+static FIROptions *sDefaultOptions = nil;
+static NSDictionary *sDefaultOptionsDictionary = nil;
+
+#pragma mark - Public only for internal class methods
+
++ (FIROptions *)defaultOptions {
+ if (sDefaultOptions != nil) {
+ return sDefaultOptions;
+ }
+
+ NSDictionary *defaultOptionsDictionary = [self defaultOptionsDictionary];
+ if (defaultOptionsDictionary == nil) {
+ return nil;
+ }
+
+ sDefaultOptions =
+ [[FIROptions alloc] initInternalWithOptionsDictionary:defaultOptionsDictionary];
+ return sDefaultOptions;
+}
+
+#pragma mark - Private class methods
+
++ (NSDictionary *)defaultOptionsDictionary {
+ if (sDefaultOptionsDictionary != nil) {
+ return sDefaultOptionsDictionary;
+ }
+ NSString *plistFilePath = [FIROptions plistFilePathWithName:kServiceInfoFileName];
+ if (plistFilePath == nil) {
+ return nil;
+ }
+ sDefaultOptionsDictionary = [NSDictionary dictionaryWithContentsOfFile:plistFilePath];
+ if (sDefaultOptionsDictionary == nil) {
+ FIRLogError(kFIRLoggerCore, @"I-COR000011", @"The configuration file is not a dictionary: "
+ @"'%@.%@'.", kServiceInfoFileName, kServiceInfoFileType);
+ }
+ return sDefaultOptionsDictionary;
+}
+
+// Returns the path of the plist file with a given file name.
++ (NSString *)plistFilePathWithName:(NSString *)fileName {
+ NSArray *bundles = [FIRBundleUtil relevantBundles];
+ NSString *plistFilePath =
+ [FIRBundleUtil optionsDictionaryPathWithResourceName:fileName
+ andFileType:kServiceInfoFileType
+ inBundles:bundles];
+ if (plistFilePath == nil) {
+ FIRLogError(kFIRLoggerCore, @"I-COR000012", @"Could not locate configuration file: '%@.%@'.",
+ fileName, kServiceInfoFileType);
+ }
+ return plistFilePath;
+}
+
++ (void)resetDefaultOptions {
+ sDefaultOptions = nil;
+ sDefaultOptionsDictionary = nil;
+}
+
+#pragma mark - Private instance methods
+
+- (instancetype)initInternalWithOptionsDictionary:(NSDictionary *)optionsDictionary {
+ self = [super init];
+ if (self) {
+ _optionsDictionary = [optionsDictionary mutableCopy];
+ _usingOptionsFromDefaultPlist = YES;
+ }
+ return self;
+}
+
+- (id)copyWithZone:(NSZone *)zone {
+ FIROptions *newOptions = [[[self class] allocWithZone:zone] init];
+ if (newOptions) {
+ newOptions.optionsDictionary = self.optionsDictionary;
+ newOptions.deepLinkURLScheme = self.deepLinkURLScheme;
+ newOptions.editingLocked = self.isEditingLocked;
+ newOptions.usingOptionsFromDefaultPlist = self.usingOptionsFromDefaultPlist;
+ }
+ return newOptions;
+}
+
+#pragma mark - Public instance methods
+
+- (instancetype)initWithGoogleAppID:(NSString *)googleAppID
+ bundleID:(NSString *)bundleID
+ GCMSenderID:(NSString *)GCMSenderID
+ APIKey:(NSString *)APIKey
+ clientID:(NSString *)clientID
+ trackingID:(NSString *)trackingID
+ androidClientID:(NSString *)androidClientID
+ databaseURL:(NSString *)databaseURL
+ storageBucket:(NSString *)storageBucket
+ deepLinkURLScheme:(NSString *)deepLinkURLScheme {
+ self = [super init];
+ if (self) {
+ if (!googleAppID) {
+ [NSException raise:kFirebaseCoreErrorDomain format:@"Please specify a valid Google App ID."];
+ } else if (!GCMSenderID) {
+ [NSException raise:kFirebaseCoreErrorDomain format:@"Please specify a valid GCM Sender ID."];
+ }
+
+ // `bundleID` is a required property, default to the main `bundleIdentifier` if it's `nil`.
+ if (!bundleID) {
+ bundleID = [[NSBundle mainBundle] bundleIdentifier];
+ }
+
+ NSMutableDictionary *mutableOptionsDict = [NSMutableDictionary dictionary];
+ [mutableOptionsDict setValue:googleAppID forKey:kFIRGoogleAppID];
+ [mutableOptionsDict setValue:bundleID forKey:kFIRBundleID];
+ [mutableOptionsDict setValue:GCMSenderID forKey:kFIRGCMSenderID];
+ [mutableOptionsDict setValue:APIKey forKey:kFIRAPIKey];
+ [mutableOptionsDict setValue:clientID forKey:kFIRClientID];
+ [mutableOptionsDict setValue:trackingID forKey:kFIRTrackingID];
+ [mutableOptionsDict setValue:androidClientID forKey:kFIRAndroidClientID];
+ [mutableOptionsDict setValue:databaseURL forKey:kFIRDatabaseURL];
+ [mutableOptionsDict setValue:storageBucket forKey:kFIRStorageBucket];
+ self.optionsDictionary = mutableOptionsDict;
+ self.deepLinkURLScheme = deepLinkURLScheme;
+ }
+ return self;
+}
+
+- (instancetype)initWithContentsOfFile:(NSString *)plistPath {
+ self = [super init];
+ if (self) {
+ if (plistPath == nil) {
+ FIRLogError(kFIRLoggerCore, @"I-COR000013", @"The plist file path is nil.");
+ return nil;
+ }
+ _optionsDictionary = [[NSDictionary dictionaryWithContentsOfFile:plistPath] mutableCopy];
+ if (_optionsDictionary == nil) {
+ FIRLogError(kFIRLoggerCore, @"I-COR000014", @"The configuration file at %@ does not exist or "
+ @"is not a well-formed plist file.", plistPath);
+ return nil;
+ }
+ // TODO: Do we want to validate the dictionary here? It says we do that already in
+ // the public header.
+ }
+ return self;
+}
+
+- (instancetype)initWithGoogleAppID:(NSString *)googleAppID
+ GCMSenderID:(NSString *)GCMSenderID {
+ self = [super init];
+ if (self) {
+ NSMutableDictionary *mutableOptionsDict = [NSMutableDictionary dictionary];
+ [mutableOptionsDict setValue:googleAppID forKey:kFIRGoogleAppID];
+ [mutableOptionsDict setValue:GCMSenderID forKey:kFIRGCMSenderID];
+ [mutableOptionsDict setValue:[[NSBundle mainBundle] bundleIdentifier] forKey:kFIRBundleID];
+ self.optionsDictionary = mutableOptionsDict;
+ }
+ return self;
+}
+
+- (NSString *)APIKey {
+ return self.optionsDictionary[kFIRAPIKey];
+}
+
+- (void)setAPIKey:(NSString *)APIKey {
+ if (self.isEditingLocked) {
+ [NSException raise:kFirebaseCoreErrorDomain format:kFIRExceptionBadModification];
+ }
+
+ _optionsDictionary[kFIRAPIKey] = [APIKey copy];
+}
+
+- (NSString *)clientID {
+ return self.optionsDictionary[kFIRClientID];
+}
+
+- (void)setClientID:(NSString *)clientID {
+ if (self.isEditingLocked) {
+ [NSException raise:kFirebaseCoreErrorDomain format:kFIRExceptionBadModification];
+ }
+
+ _optionsDictionary[kFIRClientID] = [clientID copy];
+}
+
+- (NSString *)trackingID {
+ return self.optionsDictionary[kFIRTrackingID];
+}
+
+- (void)setTrackingID:(NSString *)trackingID {
+ if (self.isEditingLocked) {
+ [NSException raise:kFirebaseCoreErrorDomain format:kFIRExceptionBadModification];
+ }
+
+ _optionsDictionary[kFIRTrackingID] = [trackingID copy];
+}
+
+- (NSString *)GCMSenderID {
+ return self.optionsDictionary[kFIRGCMSenderID];
+}
+
+- (void)setGCMSenderID:(NSString *)GCMSenderID {
+ if (self.isEditingLocked) {
+ [NSException raise:kFirebaseCoreErrorDomain format:kFIRExceptionBadModification];
+ }
+
+ _optionsDictionary[kFIRGCMSenderID] = [GCMSenderID copy];
+}
+
+- (NSString *)projectID {
+ return self.optionsDictionary[kFIRProjectID];
+}
+
+- (void)setProjectID:(NSString *)projectID {
+ if (self.isEditingLocked) {
+ [NSException raise:kFirebaseCoreErrorDomain format:kFIRExceptionBadModification];
+ }
+
+ _optionsDictionary[kFIRProjectID] = [projectID copy];
+}
+
+- (NSString *)androidClientID {
+ return self.optionsDictionary[kFIRAndroidClientID];
+}
+
+- (void)setAndroidClientID:(NSString *)androidClientID {
+ if (self.isEditingLocked) {
+ [NSException raise:kFirebaseCoreErrorDomain format:kFIRExceptionBadModification];
+ }
+
+ _optionsDictionary[kFIRAndroidClientID] = [androidClientID copy];
+}
+
+- (NSString *)googleAppID {
+ return self.optionsDictionary[kFIRGoogleAppID];
+}
+
+- (void)setGoogleAppID:(NSString *)googleAppID {
+ if (self.isEditingLocked) {
+ [NSException raise:kFirebaseCoreErrorDomain format:kFIRExceptionBadModification];
+ }
+
+ _optionsDictionary[kFIRGoogleAppID] = [googleAppID copy];
+}
+
+- (NSString *)libraryVersionID {
+ return kFIRLibraryVersionID;
+}
+
+- (void)setLibraryVersionID:(NSString *)libraryVersionID {
+ _optionsDictionary[kFIRLibraryVersionID] = [libraryVersionID copy];
+}
+
+- (NSString *)databaseURL {
+ return self.optionsDictionary[kFIRDatabaseURL];
+}
+
+- (void)setDatabaseURL:(NSString *)databaseURL {
+ if (self.isEditingLocked) {
+ [NSException raise:kFirebaseCoreErrorDomain format:kFIRExceptionBadModification];
+ }
+
+ _optionsDictionary[kFIRDatabaseURL] = [databaseURL copy];
+}
+
+- (NSString *)storageBucket {
+ return self.optionsDictionary[kFIRStorageBucket];
+}
+
+- (void)setStorageBucket:(NSString *)storageBucket {
+ if (self.isEditingLocked) {
+ [NSException raise:kFirebaseCoreErrorDomain format:kFIRExceptionBadModification];
+ }
+
+ _optionsDictionary[kFIRStorageBucket] = [storageBucket copy];
+}
+
+- (void)setDeepLinkURLScheme:(NSString *)deepLinkURLScheme {
+ if (self.isEditingLocked) {
+ [NSException raise:kFirebaseCoreErrorDomain format:kFIRExceptionBadModification];
+ }
+
+ _deepLinkURLScheme = [deepLinkURLScheme copy];
+}
+
+- (NSString *)bundleID {
+ return self.optionsDictionary[kFIRBundleID];
+}
+
+- (void)setBundleID:(NSString *)bundleID {
+ if (self.isEditingLocked) {
+ [NSException raise:kFirebaseCoreErrorDomain format:kFIRExceptionBadModification];
+ }
+
+ _optionsDictionary[kFIRBundleID] = [bundleID copy];
+}
+
+#pragma mark - Internal instance methods
+
+- (NSDictionary *)analyticsOptionsDictionary {
+ dispatch_once(&_createAnalyticsOptionsDictionaryOnce, ^{
+ NSMutableDictionary *tempAnalyticsOptions = [[NSMutableDictionary alloc] init];
+ NSDictionary *mainInfoDictionary = [NSBundle mainBundle].infoDictionary;
+ NSArray *measurementKeys = @[ kFIRIsMeasurementEnabled,
+ kFIRIsAnalyticsCollectionEnabled,
+ kFIRIsAnalyticsCollectionDeactivated ];
+ for (NSString *key in measurementKeys) {
+ id value = mainInfoDictionary[key] ?: self.optionsDictionary[key] ?: nil;
+ if (!value) {
+ continue;
+ }
+ tempAnalyticsOptions[key] = value;
+ }
+ _analyticsOptionsDictionary = tempAnalyticsOptions;
+ });
+ return _analyticsOptionsDictionary;
+}
+
+/**
+ * Whether or not Measurement was enabled. Measurement is enabled unless explicitly disabled in
+ * GoogleService-Info.plist. This uses the old plist flag IS_MEASUREMENT_ENABLED, which should still
+ * be supported.
+ */
+- (BOOL)isMeasurementEnabled {
+ if (self.isAnalyticsCollectionDeactivated) {
+ return NO;
+ }
+ if (!self.analyticsOptionsDictionary[kFIRIsMeasurementEnabled]) {
+ return YES; // Enable Measurement by default when the key is not in the dictionary.
+ }
+ return [self.analyticsOptionsDictionary[kFIRIsMeasurementEnabled] boolValue];
+}
+
+- (BOOL)isAnalyticsCollectionEnabled {
+ if (self.isAnalyticsCollectionDeactivated) {
+ return NO;
+ }
+ if (!self.analyticsOptionsDictionary[kFIRIsAnalyticsCollectionEnabled]) {
+ return self.isMeasurementEnabled; // Fall back to older plist flag.
+ }
+ return [self.analyticsOptionsDictionary[kFIRIsAnalyticsCollectionEnabled] boolValue];
+}
+
+- (BOOL)isAnalyticsCollectionDeactivated {
+ if (!self.analyticsOptionsDictionary[kFIRIsAnalyticsCollectionDeactivated]) {
+ return NO; // Analytics Collection is not deactivated when the key is not in the dictionary.
+ }
+ return [self.analyticsOptionsDictionary[kFIRIsAnalyticsCollectionDeactivated] boolValue];
+}
+
+- (BOOL)isAnalyticsEnabled {
+ return [self.optionsDictionary[kFIRIsAnalyticsEnabled] boolValue];
+}
+
+- (BOOL)isSignInEnabled {
+ return [self.optionsDictionary[kFIRIsSignInEnabled] boolValue];
+}
+
+@end
diff --git a/Firebase/Core/FIRReachabilityChecker.m b/Firebase/Core/FIRReachabilityChecker.m
new file mode 100644
index 0000000..66b6547
--- /dev/null
+++ b/Firebase/Core/FIRReachabilityChecker.m
@@ -0,0 +1,245 @@
+// Copyright 2017 Google
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "Private/FIRReachabilityChecker.h"
+#import "Private/FIRReachabilityChecker+Internal.h"
+
+#import "Private/FIRNetwork.h"
+#import "Private/FIRNetworkMessageCode.h"
+#import "Private/FIRLogger.h"
+
+static void ReachabilityCallback(SCNetworkReachabilityRef reachability,
+ SCNetworkReachabilityFlags flags, void *info);
+
+static const struct FIRReachabilityApi kFIRDefaultReachabilityApi = {
+ SCNetworkReachabilityCreateWithName,
+ SCNetworkReachabilitySetCallback,
+ SCNetworkReachabilityScheduleWithRunLoop,
+ SCNetworkReachabilityUnscheduleFromRunLoop,
+ CFRelease,
+};
+
+static NSString *const kFIRReachabilityUnknownStatus = @"Unknown";
+static NSString *const kFIRReachabilityConnectedStatus = @"Connected";
+static NSString *const kFIRReachabilityDisconnectedStatus = @"Disconnected";
+
+@interface FIRReachabilityChecker ()
+
+@property(nonatomic, assign) const struct FIRReachabilityApi *reachabilityApi;
+@property(nonatomic, assign) FIRReachabilityStatus reachabilityStatus;
+@property(nonatomic, copy) NSString *host;
+@property(nonatomic, assign) SCNetworkReachabilityRef reachability;
+
+@end
+
+@implementation FIRReachabilityChecker
+
+@synthesize reachabilityApi = reachabilityApi_;
+@synthesize reachability = reachability_;
+
+- (const struct FIRReachabilityApi *)reachabilityApi {
+ return reachabilityApi_;
+}
+
+- (void)setReachabilityApi:(const struct FIRReachabilityApi *)reachabilityApi {
+ if (reachability_) {
+ NSString *message = @"Cannot change reachability API while reachability is running. "
+ @"Call stop first.";
+ [loggerDelegate_ firNetwork_logWithLevel:kFIRNetworkLogLevelError
+ messageCode:kFIRNetworkMessageCodeReachabilityChecker000
+ message:message];
+ return;
+ }
+ reachabilityApi_ = reachabilityApi;
+}
+
+@synthesize reachabilityStatus = reachabilityStatus_;
+@synthesize host = host_;
+@synthesize reachabilityDelegate = reachabilityDelegate_;
+@synthesize loggerDelegate = loggerDelegate_;
+
+- (BOOL)isActive {
+ return reachability_ != nil;
+}
+
+- (void)setReachabilityDelegate:(id<FIRReachabilityDelegate>)reachabilityDelegate {
+ if (reachabilityDelegate &&
+ (![(NSObject *)reachabilityDelegate conformsToProtocol:@protocol(FIRReachabilityDelegate)])) {
+ FIRLogError(
+ kFIRLoggerCore,
+ [NSString stringWithFormat:@"I-NET%06ld",
+ (long)kFIRNetworkMessageCodeReachabilityChecker005],
+ @"Reachability delegate doesn't conform to Reachability protocol.");
+ return;
+ }
+ reachabilityDelegate_ = reachabilityDelegate;
+}
+
+- (void)setLoggerDelegate:(id<FIRNetworkLoggerDelegate>)loggerDelegate {
+ if (loggerDelegate &&
+ (![(NSObject *)loggerDelegate conformsToProtocol:@protocol(FIRNetworkLoggerDelegate)])) {
+ FIRLogError(
+ kFIRLoggerCore,
+ [NSString stringWithFormat:@"I-NET%06ld",
+ (long)kFIRNetworkMessageCodeReachabilityChecker006],
+ @"Reachability delegate doesn't conform to Logger protocol.");
+ return;
+ }
+ loggerDelegate_ = loggerDelegate;
+}
+
+- (instancetype)initWithReachabilityDelegate:(id<FIRReachabilityDelegate>)reachabilityDelegate
+ loggerDelegate:(id<FIRNetworkLoggerDelegate>)loggerDelegate
+ withHost:(NSString *)host {
+ self = [super init];
+
+ [self setLoggerDelegate:loggerDelegate];
+
+ if (!host || !host.length) {
+ [loggerDelegate_ firNetwork_logWithLevel:kFIRNetworkLogLevelError
+ messageCode:kFIRNetworkMessageCodeReachabilityChecker001
+ message:@"Invalid host specified"];
+ return nil;
+ }
+ if (self) {
+ [self setReachabilityDelegate:reachabilityDelegate];
+ reachabilityApi_ = &kFIRDefaultReachabilityApi;
+ reachabilityStatus_ = kFIRReachabilityUnknown;
+ host_ = [host copy];
+ reachability_ = nil;
+ }
+ return self;
+}
+
+- (void)dealloc {
+ reachabilityDelegate_ = nil;
+ loggerDelegate_ = nil;
+ [self stop];
+}
+
+- (BOOL)start {
+ if (!reachability_) {
+ reachability_ = reachabilityApi_->createWithNameFn(kCFAllocatorDefault, [host_ UTF8String]);
+ if (!reachability_) {
+ return NO;
+ }
+ SCNetworkReachabilityContext context = {
+ 0, /* version */
+ (__bridge void *)(self), /* info (passed as last parameter to reachability callback) */
+ NULL, /* retain */
+ NULL, /* release */
+ NULL /* copyDescription */
+ };
+ if (!reachabilityApi_->setCallbackFn(reachability_, ReachabilityCallback, &context) ||
+ !reachabilityApi_->scheduleWithRunLoopFn(reachability_, CFRunLoopGetMain(),
+ kCFRunLoopCommonModes)) {
+ reachabilityApi_->releaseFn(reachability_);
+ reachability_ = nil;
+ [loggerDelegate_ firNetwork_logWithLevel:kFIRNetworkLogLevelError
+ messageCode:kFIRNetworkMessageCodeReachabilityChecker002
+ message:@"Failed to start reachability handle"];
+ return NO;
+ }
+ }
+ [loggerDelegate_ firNetwork_logWithLevel:kFIRNetworkLogLevelDebug
+ messageCode:kFIRNetworkMessageCodeReachabilityChecker003
+ message:@"Monitoring the network status"];
+ return YES;
+}
+
+- (void)stop {
+ if (reachability_) {
+ reachabilityStatus_ = kFIRReachabilityUnknown;
+ reachabilityApi_->unscheduleFromRunLoopFn(reachability_, CFRunLoopGetMain(),
+ kCFRunLoopCommonModes);
+ reachabilityApi_->releaseFn(reachability_);
+ reachability_ = nil;
+ }
+}
+
+- (FIRReachabilityStatus)statusForFlags:(SCNetworkReachabilityFlags)flags {
+ FIRReachabilityStatus status = kFIRReachabilityNotReachable;
+ // If the Reachable flag is not set, we definitely don't have connectivity.
+ if (flags & kSCNetworkReachabilityFlagsReachable) {
+ // Reachable flag is set. Check further flags.
+ if (!(flags & kSCNetworkReachabilityFlagsConnectionRequired)) {
+ // Connection required flag is not set, so we have connectivity.
+ status = (flags & kSCNetworkReachabilityFlagsIsWWAN) ? kFIRReachabilityViaCellular
+ : kFIRReachabilityViaWifi;
+ } else if ((flags & (kSCNetworkReachabilityFlagsConnectionOnDemand |
+ kSCNetworkReachabilityFlagsConnectionOnTraffic)) &&
+ !(flags & kSCNetworkReachabilityFlagsInterventionRequired)) {
+ // If the connection on demand or connection on traffic flag is set, and user intervention
+ // is not required, we have connectivity.
+ status = (flags & kSCNetworkReachabilityFlagsIsWWAN) ? kFIRReachabilityViaCellular
+ : kFIRReachabilityViaWifi;
+ }
+ }
+ return status;
+}
+
+- (void)reachabilityFlagsChanged:(SCNetworkReachabilityFlags)flags {
+ FIRReachabilityStatus status = [self statusForFlags:flags];
+ if (reachabilityStatus_ != status) {
+ NSString *reachabilityStatusString;
+ if (status == kFIRReachabilityUnknown) {
+ reachabilityStatusString = kFIRReachabilityUnknownStatus;
+ } else {
+ reachabilityStatusString = (status == kFIRReachabilityNotReachable)
+ ? kFIRReachabilityDisconnectedStatus
+ : kFIRReachabilityConnectedStatus;
+ }
+ [loggerDelegate_ firNetwork_logWithLevel:kFIRNetworkLogLevelDebug
+ messageCode:kFIRNetworkMessageCodeReachabilityChecker004
+ message:@"Network status has changed. Code, status"
+ contexts:@[ @(status), reachabilityStatusString ]];
+ reachabilityStatus_ = status;
+ [reachabilityDelegate_ reachability:self statusChanged:reachabilityStatus_];
+ }
+}
+
+@end
+
+static void ReachabilityCallback(SCNetworkReachabilityRef reachability,
+ SCNetworkReachabilityFlags flags, void *info) {
+ FIRReachabilityChecker *checker = (__bridge FIRReachabilityChecker *)info;
+ [checker reachabilityFlagsChanged:flags];
+}
+
+// This function used to be at the top of the file, but it was moved here
+// as a workaround for a suspected compiler bug. When compiled in Release mode
+// and run on an iOS device with WiFi disabled, the reachability code crashed
+// when calling SCNetworkReachabilityScheduleWithRunLoop, or shortly thereafter.
+// After unsuccessfully trying to diagnose the cause of the crash, it was
+// discovered that moving this function to the end of the file magically fixed
+// the crash. If you are going to edit this file, exercise caution and make sure
+// to test thoroughly with an iOS device under various network conditions.
+const NSString *FIRReachabilityStatusString(FIRReachabilityStatus status) {
+ switch (status) {
+ case kFIRReachabilityUnknown:
+ return @"Reachability Unknown";
+
+ case kFIRReachabilityNotReachable:
+ return @"Not reachable";
+
+ case kFIRReachabilityViaWifi:
+ return @"Reachable via Wifi";
+
+ case kFIRReachabilityViaCellular:
+ return @"Reachable via Cellular Data";
+
+ default:
+ return [NSString stringWithFormat:@"Invalid reachability status %d", (int)status];
+ }
+}
diff --git a/Firebase/Core/FIRURLSchemeUtil.m b/Firebase/Core/FIRURLSchemeUtil.m
new file mode 100644
index 0000000..8dbecae
--- /dev/null
+++ b/Firebase/Core/FIRURLSchemeUtil.m
@@ -0,0 +1,43 @@
+// Copyright 2017 Google
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "Private/FIRURLSchemeUtil.h"
+#import "Private/FIRLogger.h"
+
+/**
+ * Regular expression to match the URL scheme for Google sign-in.
+ */
+static NSString *const kFIRGoogleSignInURLSchemePattern =
+@"^com\\.googleusercontent\\.apps\\.\\d+-\\w+$";
+
+BOOL fir_areURLSchemesValidForGoogleSignIn(NSArray *urlSchemes) {
+ BOOL hasReversedClientID = NO;
+ for (NSString *urlScheme in urlSchemes) {
+ if (!hasReversedClientID) {
+ NSRange range = [urlScheme rangeOfString:kFIRGoogleSignInURLSchemePattern
+ options:NSRegularExpressionSearch];
+ if (range.location != NSNotFound) {
+ hasReversedClientID = YES;
+ }
+ }
+ }
+ if (hasReversedClientID) {
+ return YES;
+ }
+ if (!hasReversedClientID) {
+ FIRLogInfo(kFIRLoggerCore, @"I-COR000021", @"A reversed client ID should be added as a URL "
+ @"scheme to enable Google sign-in.");
+ }
+ return NO;
+}
diff --git a/Firebase/Core/FirebaseCore.h b/Firebase/Core/FirebaseCore.h
new file mode 100644
index 0000000..fa26f69
--- /dev/null
+++ b/Firebase/Core/FirebaseCore.h
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRAnalyticsConfiguration.h"
+#import "FIRApp.h"
+#import "FIRConfiguration.h"
+#import "FIRLoggerLevel.h"
+#import "FIROptions.h"
diff --git a/Firebase/Core/FirebaseCore.podspec b/Firebase/Core/FirebaseCore.podspec
new file mode 100644
index 0000000..f513367
--- /dev/null
+++ b/Firebase/Core/FirebaseCore.podspec
@@ -0,0 +1,35 @@
+# This podspec is not intended to be deployed. It is solely for the static
+# library framework build process at
+# https://github.com/firebase/firebase-ios-sdk/tree/master/BuildFrameworks
+
+Pod::Spec.new do |s|
+ s.name = 'FirebaseCore'
+ s.version = '4.0.0'
+ s.summary = 'Firebase Open Source Libraries for iOS.'
+
+ s.description = <<-DESC
+Simplify your iOS development, grow your user base, and monetize more effectively with Firebase.
+ DESC
+
+ s.homepage = 'https://firebase.google.com'
+ s.license = { :type => 'Apache', :file => '../../LICENSE' }
+ s.authors = 'Google, Inc.'
+
+ # NOTE that the FirebaseDev pod is neither publicly deployed nor yet interchangeable with the
+ # Firebase pod
+ s.source = { :git => 'https://github.com/firebase/firebase-ios-sdk.git', :tag => s.version.to_s }
+ s.social_media_url = 'https://twitter.com/Firebase'
+ s.ios.deployment_target = '7.0'
+
+ s.source_files = '**/*.[mh]'
+ s.public_header_files =
+ 'FirebaseCore.h',
+ 'FIRAnalyticsConfiguration.h',
+ 'FIRApp.h',
+ 'FIRConfiguration.h',
+ 'FIRLoggerLevel.h',
+ 'FIROptions.h',
+ 'FIRCoreSwiftNameSupport.h'
+
+ s.dependency 'GoogleToolboxForMac/NSData+zlib', '~> 2.1'
+end
diff --git a/Firebase/Core/Private/FIRAnalyticsConfiguration+Internal.h b/Firebase/Core/Private/FIRAnalyticsConfiguration+Internal.h
new file mode 100644
index 0000000..6c57a0f
--- /dev/null
+++ b/Firebase/Core/Private/FIRAnalyticsConfiguration+Internal.h
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "../FIRAnalyticsConfiguration.h"
+
+/// Values stored in analyticsEnabledState. Never alter these constants since they must match with
+/// values persisted to disk.
+typedef NS_ENUM(int64_t, FIRAnalyticsEnabledState) {
+ // 0 is the default value for keys not found stored in persisted config, so it cannot represent
+ // kFIRAnalyticsEnabledStateSetNo. It must represent kFIRAnalyticsEnabledStateNotSet.
+ kFIRAnalyticsEnabledStateNotSet = 0,
+ kFIRAnalyticsEnabledStateSetYes = 1,
+ kFIRAnalyticsEnabledStateSetNo = 2,
+};
+
+/// The user defaults key for the persisted measurementEnabledState value. FIRAPersistedConfig reads
+/// measurementEnabledState using this same key.
+static NSString *const kFIRAPersistedConfigMeasurementEnabledStateKey =
+ @"/google/measurement/measurement_enabled_state";
+
+static NSString *const kFIRAnalyticsConfigurationSetEnabledNotification =
+ @"FIRAnalyticsConfigurationSetEnabledNotification";
+static NSString *const kFIRAnalyticsConfigurationSetMinimumSessionIntervalNotification =
+ @"FIRAnalyticsConfigurationSetMinimumSessionIntervalNotification";
+static NSString *const kFIRAnalyticsConfigurationSetSessionTimeoutIntervalNotification =
+ @"FIRAnalyticsConfigurationSetSessionTimeoutIntervalNotification";
diff --git a/Firebase/Core/Private/FIRAppAssociationRegistration.h b/Firebase/Core/Private/FIRAppAssociationRegistration.h
new file mode 100644
index 0000000..3d697a7
--- /dev/null
+++ b/Firebase/Core/Private/FIRAppAssociationRegistration.h
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @class FIRAppAssociationRegistration
+ @brief Manages object associations as a singleton-dependent: At most one object is
+ registered for any given host/key pair, and the object shall be created on-the-fly when
+ asked for.
+ */
+@interface FIRAppAssociationRegistration<ObjectType> : NSObject
+
+/** @fn registeredObjectWithHost:key:creationBlock:
+ @brief Retrieves the registered object with a particular host and key.
+ @param host The host object.
+ @param key The key to specify the registered object on the host.
+ @param creationBlock The block to return the object to be registered if not already.
+ The block is executed immediately before this method returns if it is executed at all.
+ It can also be executed multiple times across different method invocations if previous
+ execution of the block returns @c nil.
+ @return The registered object for the host/key pair, or @c nil if no object is registered
+ and @c creationBlock returns @c nil.
+ @remarks The method is thread-safe but non-reentrant in the sense that attempting to call this
+ method again within the @c creationBlock with the same host/key pair raises an exception.
+ The registered object is retained by the host.
+ */
++ (nullable ObjectType)registeredObjectWithHost:(id)host
+ key:(NSString *)key
+ creationBlock:(ObjectType _Nullable (^)())creationBlock;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Core/Private/FIRAppEnvironmentUtil.h b/Firebase/Core/Private/FIRAppEnvironmentUtil.h
new file mode 100644
index 0000000..ba4696c
--- /dev/null
+++ b/Firebase/Core/Private/FIRAppEnvironmentUtil.h
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+#import <UIKit/UIKit.h>
+
+@interface FIRAppEnvironmentUtil : NSObject
+
+/// Indicates whether the app is from Apple Store or not. Returns NO if the app is on simulator,
+/// development environment or sideloaded.
++ (BOOL)isFromAppStore;
+
+/// Indicates whether the app is a Testflight app. Returns YES if the app has sandbox receipt.
+/// Returns NO otherwise.
++ (BOOL)isAppStoreReceiptSandbox;
+
+/// Indicates whether the app is on simulator or not at runtime depending on the device
+/// architecture.
++ (BOOL)isSimulator;
+
+/// The current device model. Returns an empty string if device model cannot be retrieved.
++ (NSString *)deviceModel;
+
+/// The current operating system version. Returns an empty string if the system version cannot be
+/// retrieved.
++ (NSString *)systemVersion;
+
+/// Indicates whether it is running inside an extension or an app.
++ (BOOL)isAppExtension;
+
+/// Returns the [UIApplication sharedApplication] if it is running on an app, not an extension.
++ (UIApplication *)sharedApplication;
+
+@end
diff --git a/Firebase/Core/Private/FIRAppInternal.h b/Firebase/Core/Private/FIRAppInternal.h
new file mode 100644
index 0000000..11b3bf6
--- /dev/null
+++ b/Firebase/Core/Private/FIRAppInternal.h
@@ -0,0 +1,140 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRApp.h"
+#import "FIRErrors.h"
+
+/**
+ * The internal interface to FIRApp. This is meant for first-party integrators, who need to receive
+ * FIRApp notifications, log info about the success or failure of their configuration, and access
+ * other internal functionality of FIRApp.
+ *
+ * TODO(b/28296561): Restructure this header.
+ */
+NS_ASSUME_NONNULL_BEGIN
+
+typedef NS_ENUM(NSInteger, FIRConfigType) {
+ FIRConfigTypeCore = 1,
+ FIRConfigTypeSDK = 2,
+};
+
+/**
+ * Names of services provided by Firebase.
+ */
+extern NSString *const kFIRServiceAdMob;
+extern NSString *const kFIRServiceAuth;
+extern NSString *const kFIRServiceCrash;
+extern NSString *const kFIRServiceDatabase;
+extern NSString *const kFIRServiceDynamicLinks;
+extern NSString *const kFIRServiceInstanceID;
+extern NSString *const kFIRServiceInvites;
+extern NSString *const kFIRServiceMessaging;
+extern NSString *const kFIRServiceMeasurement;
+extern NSString *const kFIRServiceRemoteConfig;
+extern NSString *const kFIRServiceStorage;
+
+/**
+ * Names of services provided by the Google pod, but logged by the Firebase pod.
+ */
+extern NSString *const kGGLServiceAnalytics;
+extern NSString *const kGGLServiceSignIn;
+
+extern NSString *const kFIRDefaultAppName;
+extern NSString *const kFIRAppReadyToConfigureSDKNotification;
+extern NSString *const kFIRAppDeleteNotification;
+extern NSString *const kFIRAppIsDefaultAppKey;
+extern NSString *const kFIRAppNameKey;
+extern NSString *const kFIRGoogleAppIDKey;
+
+/** @typedef FIRTokenCallback
+ @brief The type of block which gets called when a token is ready.
+ */
+typedef void (^FIRTokenCallback)(NSString *_Nullable token, NSError *_Nullable error);
+
+/** @typedef FIRAppGetTokenImplementation
+ @brief The type of block which can provide an implementation for the @c getTokenWithCallback:
+ method.
+ @param forceRefresh Forces the token to be refreshed.
+ @param callback The block which should be invoked when the async call completes.
+ */
+typedef void (^FIRAppGetTokenImplementation)(BOOL forceRefresh, FIRTokenCallback callback);
+
+/** @typedef FIRAppGetUID
+ @brief The type of block which can provide an implementation for the @c getUID method.
+ */
+typedef NSString *_Nullable (^FIRAppGetUIDImplementation)();
+
+@interface FIRApp ()
+
+/** @property getTokenImplementation
+ @brief Gets or sets the block to use for the implementation of
+ @c getTokenForcingRefresh:withCallback:
+ */
+@property(nonatomic, copy) FIRAppGetTokenImplementation getTokenImplementation;
+
+/** @property getUIDImplementation
+ @brief Gets or sets the block to use for the implementation of @c getUID.
+ */
+@property(nonatomic, copy) FIRAppGetUIDImplementation getUIDImplementation;
+
+/**
+ * Creates an error for failing to configure a subspec service. This method is called by each
+ * FIRApp notification listener.
+ */
++ (NSError *)errorForSubspecConfigurationFailureWithDomain:(NSString *)domain
+ errorCode:(FIRErrorCode)code
+ service:(NSString *)service
+ reason:(NSString *)reason;
+
+/**
+ * Used by each SDK to send logs about SDK configuration status to Clearcut.
+ */
+- (void)sendLogsWithServiceName:(NSString *)serviceName
+ version:(NSString *)version
+ error:(NSError *)error;
+
+/**
+ * Can be used by the unit tests in eack SDK to reset FIRApp. This method is thread unsafe.
+ */
++ (void)resetApps;
+
+/**
+ * Can be used by the unit tests in each SDK to set customized options.
+ */
+- (instancetype)initInstanceWithName:(NSString *)name options:(FIROptions *)options;
+
+/** @fn getTokenForcingRefresh:withCallback:
+ @brief Retrieves the Firebase authentication token, possibly refreshing it.
+ @param forceRefresh Forces a token refresh. Useful if the token becomes invalid for some reason
+ other than an expiration.
+ @param callback The block to invoke when the token is available.
+ */
+- (void)getTokenForcingRefresh:(BOOL)forceRefresh withCallback:(FIRTokenCallback)callback;
+
+/**
+ * Exposed for use by the Google pod. Configures the default app without sending notifications to
+ * other SDKs. Otherwise, behaves exactly like +configure.
+ */
++ (void)configureWithoutSendingNotification;
+
+/**
+ * Expose the UID of the current user for Firestore.
+ */
+- (nullable NSString *)getUID;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Core/Private/FIRBundleUtil.h b/Firebase/Core/Private/FIRBundleUtil.h
new file mode 100644
index 0000000..4bfef8d
--- /dev/null
+++ b/Firebase/Core/Private/FIRBundleUtil.h
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+/**
+ * This class provides utilities for accessing resources in bundles.
+ */
+@interface FIRBundleUtil : NSObject
+
+/**
+ * Finds all relevant bundles, starting with [NSBundle mainBundle].
+ */
++ (NSArray *)relevantBundles;
+
+/**
+ * Reads the options dictionary from one of the provided bundles.
+ *
+ * @param resourceName The resource name, e.g. @"GoogleService-Info".
+ * @param fileType The file type (extension), e.g. @"plist".
+ * @param bundles The bundles to expect, in priority order. See also
+ * +[FIRBundleUtil relevantBundles].
+ */
++ (NSString *)optionsDictionaryPathWithResourceName:(NSString *)resourceName
+ andFileType:(NSString *)fileType
+ inBundles:(NSArray *)bundles;
+
+/**
+ * Finds URL schemes defined in all relevant bundles, starting with those from
+ * [NSBundle mainBundle].
+ */
++ (NSArray *)relevantURLSchemes;
+
+/**
+ * Finds bundle identifiers in all relevant bundles, starting with those from [NSBundle mainBundle].
+ */
++ (NSSet *)relevantBundleIdentifiers;
+
+/**
+ * Checks if the bundle identifier exists in the given bundles.
+ */
++ (BOOL)hasBundleIdentifier:(NSString *)bundleIdentifier inBundles:(NSArray *)bundles;
+
+@end
diff --git a/Firebase/Core/Private/FIRErrorCode.h b/Firebase/Core/Private/FIRErrorCode.h
new file mode 100644
index 0000000..01d3c56
--- /dev/null
+++ b/Firebase/Core/Private/FIRErrorCode.h
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/** Error codes in Firebase error domain. */
+typedef NS_ENUM(NSInteger, FIRErrorCode) {
+ /**
+ * Unknown error.
+ */
+ FIRErrorCodeUnknown = 0,
+ /**
+ * Loading data from the GoogleService-Info.plist file failed. This is a fatal error and should
+ * not be ignored. Further calls to the API will fail and/or possibly cause crashes.
+ */
+ FIRErrorCodeInvalidPlistFile = -100,
+
+ /**
+ * Validating the Google App ID format failed.
+ */
+ FIRErrorCodeInvalidAppID = -101,
+
+ /**
+ * Error code for failing to configure a specific service.
+ */
+ FIRErrorCodeAdMobFailed = -110,
+ FIRErrorCodeAppInviteFailed = -112,
+ FIRErrorCodeCloudMessagingFailed = -113,
+ FIRErrorCodeConfigFailed = -114,
+ FIRErrorCodeDatabaseFailed = -115,
+ FIRErrorCodeCrashReportingFailed = -118,
+ FIRErrorCodeDurableDeepLinkFailed = -119,
+ FIRErrorCodeAuthFailed = -120,
+ FIRErrorCodeInstanceIDFailed = -121,
+ FIRErrorCodeStorageFailed = -123,
+
+ /**
+ * Error codes returned by Dynamic Links
+ */
+ FIRErrorCodeDynamicLinksStrongMatchNotAvailable = -124,
+ FIRErrorCodeDynamicLinksManualRetrievalNotEnabled = -125,
+ FIRErrorCodeDynamicLinksPendingLinkOnlyAvailableAtFirstLaunch = -126,
+ FIRErrorCodeDynamicLinksPendingLinkRetrievalAlreadyRunning = -127,
+};
diff --git a/Firebase/Core/Private/FIRErrors.h b/Firebase/Core/Private/FIRErrors.h
new file mode 100644
index 0000000..9a03575
--- /dev/null
+++ b/Firebase/Core/Private/FIRErrors.h
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+#include "FIRErrorCode.h"
+
+extern NSString *const kFirebaseErrorDomain;
+extern NSString *const kFirebaseAdMobErrorDomain;
+extern NSString *const kFirebaseAppInviteErrorDomain;
+extern NSString *const kFirebaseAuthErrorDomain;
+extern NSString *const kFirebaseCloudMessagingErrorDomain;
+extern NSString *const kFirebaseConfigErrorDomain;
+extern NSString *const kFirebaseCoreErrorDomain;
+extern NSString *const kFirebaseCrashReportingErrorDomain;
+extern NSString *const kFirebaseDatabaseErrorDomain;
+extern NSString *const kFirebaseDurableDeepLinkErrorDomain;
+extern NSString *const kFirebaseInstanceIDErrorDomain;
+extern NSString *const kFirebasePerfErrorDomain;
+extern NSString *const kFirebaseStorageErrorDomain;
+
+/**
+ * Factory for a NSError in the Firebase error domain.
+ *
+ * @param domain Domain of Firebase error.
+ * @param code Error code that NSError should have.
+ * @param userInfo User info that NSError should have.
+ * @return An NSError in the Firebase domain.
+ */
+extern NSError *FIRCreateError(NSString *domain, FIRErrorCode code, NSDictionary *userInfo);
diff --git a/Firebase/Core/Private/FIRLogger.h b/Firebase/Core/Private/FIRLogger.h
new file mode 100644
index 0000000..2206c0a
--- /dev/null
+++ b/Firebase/Core/Private/FIRLogger.h
@@ -0,0 +1,115 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+#import "FIRLoggerLevel.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * The Firebase services used in Firebase logger.
+ */
+typedef NSString *const FIRLoggerService;
+
+extern FIRLoggerService kFIRLoggerABTesting;
+extern FIRLoggerService kFIRLoggerAdMob;
+extern FIRLoggerService kFIRLoggerAnalytics;
+extern FIRLoggerService kFIRLoggerAuth;
+extern FIRLoggerService kFIRLoggerCore;
+extern FIRLoggerService kFIRLoggerCrash;
+extern FIRLoggerService kFIRLoggerDatabase;
+extern FIRLoggerService kFIRLoggerDynamicLinks;
+extern FIRLoggerService kFIRLoggerInstanceID;
+extern FIRLoggerService kFIRLoggerInvites;
+extern FIRLoggerService kFIRLoggerMessaging;
+extern FIRLoggerService kFIRLoggerPerf;
+extern FIRLoggerService kFIRLoggerRemoteConfig;
+extern FIRLoggerService kFIRLoggerStorage;
+
+/**
+ * Enables or disables Analytics debug mode.
+ * If set to YES, the logging level for Analytics will be set to FIRLoggerLevelDebug.
+ * Enabling the debug mode has no effect if the app is running from App Store.
+ * (required) analytics debug mode flag.
+ */
+void FIRSetAnalyticsDebugMode(BOOL analyticsDebugMode);
+
+/**
+ * Changes the default logging level of FIRLoggerLevelNotice to a user-specified level.
+ * The default level cannot be set above FIRLoggerLevelNotice if the app is running from App Store.
+ * (required) log level (one of the FIRLoggerLevel enum values).
+ */
+void FIRSetLoggerLevel(FIRLoggerLevel loggerLevel);
+
+/**
+ * Checks if the specified logger level is loggable given the current settings.
+ * (required) log level (one of the FIRLoggerLevel enum values).
+ * (required) whether or not this function is called from the Analytics component.
+ */
+BOOL FIRIsLoggableLevel(FIRLoggerLevel loggerLevel, BOOL analyticsComponent);
+
+/**
+ * Logs a message to the Xcode console and the device log. If running from AppStore, will
+ * not log any messages with a level higher than FIRLoggerLevelNotice to avoid log spamming.
+ * (required) log level (one of the FIRLoggerLevel enum values).
+ * (required) service name of type FIRLoggerService.
+ * (required) message code starting with "I-" which means iOS, followed by a capitalized
+ * three-character service identifier and a six digit integer message ID that is unique
+ * within the service.
+ * An example of the message code is @"I-COR000001".
+ * (required) message string which can be a format string.
+ * (optional) variable arguments list obtained from calling va_start, used when message is a format
+ * string.
+ */
+extern void FIRLogBasic(FIRLoggerLevel level,
+ FIRLoggerService service,
+ NSString *messageCode,
+ NSString *message,
+// On 64-bit simulators, va_list is not a pointer, so cannot be marked nullable
+// See: http://stackoverflow.com/q/29095469
+#if __LP64__ && TARGET_IPHONE_SIMULATOR
+ va_list args_ptr
+#else
+ va_list _Nullable args_ptr
+#endif
+ );
+
+/**
+ * The following functions accept the following parameters in order:
+ * (required) service name of type FIRLoggerService.
+ * (required) message code starting from "I-" which means iOS, followed by a capitalized
+ * three-character service identifier and a six digit integer message ID that is unique
+ * within the service.
+ * An example of the message code is @"I-COR000001".
+ * See go/firebase-log-proposal for details.
+ * (required) message string which can be a format string.
+ * (optional) the list of arguments to substitute into the format string.
+ * Example usage:
+ * FIRLogError(kFIRLoggerCore, @"I-COR000001", @"Configuration of %@ failed.", app.name);
+ */
+extern void FIRLogError(FIRLoggerService service, NSString *messageCode, NSString *message, ...)
+ NS_FORMAT_FUNCTION(3, 4);
+extern void FIRLogWarning(FIRLoggerService service, NSString *messageCode, NSString *message, ...)
+ NS_FORMAT_FUNCTION(3, 4);
+extern void FIRLogNotice(FIRLoggerService service, NSString *messageCode, NSString *message, ...)
+ NS_FORMAT_FUNCTION(3, 4);
+extern void FIRLogInfo(FIRLoggerService service, NSString *messageCode, NSString *message, ...)
+ NS_FORMAT_FUNCTION(3, 4);
+extern void FIRLogDebug(FIRLoggerService service, NSString *messageCode, NSString *message, ...)
+ NS_FORMAT_FUNCTION(3, 4);
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Core/Private/FIRMutableDictionary.h b/Firebase/Core/Private/FIRMutableDictionary.h
new file mode 100644
index 0000000..ebe2d33
--- /dev/null
+++ b/Firebase/Core/Private/FIRMutableDictionary.h
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@import Foundation;
+
+/// A mutable dictionary that provides atomic accessor and mutators.
+@interface FIRMutableDictionary : NSObject
+
+/// Returns an object given a key in the dictionary or nil if not found.
+- (id)objectForKey:(id)key;
+
+/// Updates the object given its key or adds it to the dictionary if it is not in the dictionary.
+- (void)setObject:(id)object forKey:(id<NSCopying>)key;
+
+/// Removes the object given its session ID from the dictionary.
+- (void)removeObjectForKey:(id)key;
+
+/// Removes all objects.
+- (void)removeAllObjects;
+
+/// Returns the number of current objects in the dictionary.
+- (NSUInteger)count;
+
+/// Returns an object given a key in the dictionary or nil if not found.
+- (id)objectForKeyedSubscript:(id<NSCopying>)key;
+
+/// Updates the object given its key or adds it to the dictionary if it is not in the dictionary.
+- (void)setObject:(id)obj forKeyedSubscript:(id<NSCopying>)key;
+
+/// Returns the immutable dictionary.
+- (NSDictionary *)dictionary;
+
+@end
diff --git a/Firebase/Core/Private/FIRNetwork.h b/Firebase/Core/Private/FIRNetwork.h
new file mode 100644
index 0000000..aac0bca
--- /dev/null
+++ b/Firebase/Core/Private/FIRNetwork.h
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@import Foundation;
+
+#import "FIRNetworkConstants.h"
+#import "FIRNetworkLoggerProtocol.h"
+#import "FIRNetworkURLSession.h"
+
+/// Delegate protocol for FIRNetwork events.
+@protocol FIRNetworkReachabilityDelegate
+
+/// Tells the delegate to handle events when the network reachability changes to connected or not
+/// connected.
+- (void)reachabilityDidChange;
+
+@end
+
+/// The Network component that provides network status and handles network requests and responses.
+/// This is not thread safe.
+///
+/// NOTE:
+/// User must add FIRAnalytics handleEventsForBackgroundURLSessionID:completionHandler to the
+/// AppDelegate application:handleEventsForBackgroundURLSession:completionHandler:
+@interface FIRNetwork : NSObject
+
+/// Indicates if network connectivity is available.
+@property(nonatomic, readonly, getter=isNetworkConnected) BOOL networkConnected;
+
+/// Indicates if there are any uploads in progress.
+@property(nonatomic, readonly, getter=hasUploadInProgress) BOOL uploadInProgress;
+
+/// An optional delegate that can be used in the event when network reachability changes.
+@property(nonatomic, weak) id<FIRNetworkReachabilityDelegate> reachabilityDelegate;
+
+/// An optional delegate that can be used to log messages, warnings or errors that occur in the
+/// network operations.
+@property(nonatomic, weak) id<FIRNetworkLoggerDelegate> loggerDelegate;
+
+/// Indicates whether the logger should display debug messages.
+@property(nonatomic, assign) BOOL isDebugModeEnabled;
+
+/// The time interval in seconds for the network request to timeout.
+@property(nonatomic, assign) NSTimeInterval timeoutInterval;
+
+/// Initializes with the default reachability host.
+- (instancetype)init;
+
+/// Initializes with a custom reachability host.
+- (instancetype)initWithReachabilityHost:(NSString *)reachabilityHost;
+
+/// Handles events when background session with the given ID has finished.
++ (void)handleEventsForBackgroundURLSessionID:(NSString *)sessionID
+ completionHandler:(FIRNetworkSystemCompletionHandler)completionHandler;
+
+/// Compresses and sends a POST request with the provided data to the URL. The session will be
+/// background session if usingBackgroundSession is YES. Otherwise, the POST session is default
+/// session. Returns a session ID or nil if an error occurs.
+- (NSString *)postURL:(NSURL *)url
+ payload:(NSData *)payload
+ queue:(dispatch_queue_t)queue
+ usingBackgroundSession:(BOOL)usingBackgroundSession
+ completionHandler:(FIRNetworkCompletionHandler)handler;
+
+/// Sends a GET request with the provided data to the URL. The session will be background session
+/// if usingBackgroundSession is YES. Otherwise, the GET session is default session. Returns a
+/// session ID or nil if an error occurs.
+- (NSString *)getURL:(NSURL *)url
+ headers:(NSDictionary *)headers
+ queue:(dispatch_queue_t)queue
+ usingBackgroundSession:(BOOL)usingBackgroundSession
+ completionHandler:(FIRNetworkCompletionHandler)handler;
+
+@end
diff --git a/Firebase/Core/Private/FIRNetworkConstants.h b/Firebase/Core/Private/FIRNetworkConstants.h
new file mode 100644
index 0000000..5878088
--- /dev/null
+++ b/Firebase/Core/Private/FIRNetworkConstants.h
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@import Foundation;
+
+/// Error codes in Firebase Network error domain.
+/// Note: these error codes should never change. It would make it harder to decode the errors if
+/// we inadvertently altered any of these codes in a future SDK version.
+typedef NS_ENUM(NSInteger, FIRNetworkErrorCode) {
+ /// Unknown error.
+ FIRNetworkErrorCodeUnknown = 0,
+ /// Error occurs when the request URL is invalid.
+ FIRErrorCodeNetworkInvalidURL = 1,
+ /// Error occurs when request cannot be constructed.
+ FIRErrorCodeNetworkRequestCreation = 2,
+ /// Error occurs when payload cannot be compressed.
+ FIRErrorCodeNetworkPayloadCompression = 3,
+ /// Error occurs when session task cannot be created.
+ FIRErrorCodeNetworkSessionTaskCreation = 4,
+ /// Error occurs when there is no response.
+ FIRErrorCodeNetworkInvalidResponse = 5
+};
+
+#pragma mark - Network constants
+
+/// The prefix of the ID of the background session.
+extern NSString *const kFIRNetworkBackgroundSessionConfigIDPrefix;
+
+/// The sub directory to store the files of data that is being uploaded in the background.
+extern NSString *const kFIRNetworkApplicationSupportSubdirectory;
+
+/// Name of the temporary directory that stores files for background uploading.
+extern NSString *const kFIRNetworkTempDirectoryName;
+
+/// The period when the temporary uploading file can stay.
+extern const NSTimeInterval kFIRNetworkTempFolderExpireTime;
+
+/// The default network request timeout interval.
+extern const NSTimeInterval kFIRNetworkTimeOutInterval;
+
+/// The host to check the reachability of the network.
+extern NSString *const kFIRNetworkReachabilityHost;
+
+/// The key to get the error context of the UserInfo.
+extern NSString *const kFIRNetworkErrorContext;
+
+#pragma mark - Network Status Code
+
+extern const int kFIRNetworkHTTPStatusOK;
+extern const int kFIRNetworkHTTPStatusNoContent;
+extern const int kFIRNetworkHTTPStatusCodeMultipleChoices;
+extern const int kFIRNetworkHTTPStatusCodeMovedPermanently;
+extern const int kFIRNetworkHTTPStatusCodeFound;
+extern const int kFIRNetworkHTTPStatusCodeNotModified;
+extern const int kFIRNetworkHTTPStatusCodeMovedTemporarily;
+extern const int kFIRNetworkHTTPStatusCodeNotFound;
+extern const int kFIRNetworkHTTPStatusCodeCannotAcceptTraffic;
+extern const int kFIRNetworkHTTPStatusCodeUnavailable;
+
+#pragma mark - Error Domain
+
+extern NSString *const kFIRNetworkErrorDomain;
diff --git a/Firebase/Core/Private/FIRNetworkLoggerProtocol.h b/Firebase/Core/Private/FIRNetworkLoggerProtocol.h
new file mode 100644
index 0000000..7b7d094
--- /dev/null
+++ b/Firebase/Core/Private/FIRNetworkLoggerProtocol.h
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@import Foundation;
+
+#import "FIRNetworkMessageCode.h"
+#import "../FIRLoggerLevel.h"
+
+/// The log levels used by FIRNetworkLogger.
+typedef NS_ENUM(NSInteger, FIRNetworkLogLevel) {
+ kFIRNetworkLogLevelError = FIRLoggerLevelError,
+ kFIRNetworkLogLevelWarning = FIRLoggerLevelWarning,
+ kFIRNetworkLogLevelInfo = FIRLoggerLevelInfo,
+ kFIRNetworkLogLevelDebug = FIRLoggerLevelDebug,
+};
+
+@protocol FIRNetworkLoggerDelegate<NSObject>
+
+@required
+/// Tells the delegate to log a message with an array of contexts and the log level.
+- (void)firNetwork_logWithLevel:(FIRNetworkLogLevel)logLevel
+ messageCode:(FIRNetworkMessageCode)messageCode
+ message:(NSString *)message
+ contexts:(NSArray *)contexts;
+
+/// Tells the delegate to log a message with a context and the log level.
+- (void)firNetwork_logWithLevel:(FIRNetworkLogLevel)logLevel
+ messageCode:(FIRNetworkMessageCode)messageCode
+ message:(NSString *)message
+ context:(id)context;
+
+/// Tells the delegate to log a message with the log level.
+- (void)firNetwork_logWithLevel:(FIRNetworkLogLevel)logLevel
+ messageCode:(FIRNetworkMessageCode)messageCode
+ message:(NSString *)message;
+
+@end
diff --git a/Firebase/Core/Private/FIRNetworkMessageCode.h b/Firebase/Core/Private/FIRNetworkMessageCode.h
new file mode 100644
index 0000000..30f562f
--- /dev/null
+++ b/Firebase/Core/Private/FIRNetworkMessageCode.h
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Make sure these codes do not overlap with any contained in the FIRAMessageCode enum.
+typedef NS_ENUM(NSInteger, FIRNetworkMessageCode) {
+ // FIRNetwork.m
+ kFIRNetworkMessageCodeNetwork000 = 900000, // I-NET900000
+ kFIRNetworkMessageCodeNetwork001 = 900001, // I-NET900001
+ kFIRNetworkMessageCodeNetwork002 = 900002, // I-NET900002
+ kFIRNetworkMessageCodeNetwork003 = 900003, // I-NET900003
+ // FIRNetworkURLSession.m
+ kFIRNetworkMessageCodeURLSession000 = 901000, // I-NET901000
+ kFIRNetworkMessageCodeURLSession001 = 901001, // I-NET901001
+ kFIRNetworkMessageCodeURLSession002 = 901002, // I-NET901002
+ kFIRNetworkMessageCodeURLSession003 = 901003, // I-NET901003
+ kFIRNetworkMessageCodeURLSession004 = 901004, // I-NET901004
+ kFIRNetworkMessageCodeURLSession005 = 901005, // I-NET901005
+ kFIRNetworkMessageCodeURLSession006 = 901006, // I-NET901006
+ kFIRNetworkMessageCodeURLSession007 = 901007, // I-NET901007
+ kFIRNetworkMessageCodeURLSession008 = 901008, // I-NET901008
+ kFIRNetworkMessageCodeURLSession009 = 901009, // I-NET901009
+ kFIRNetworkMessageCodeURLSession010 = 901010, // I-NET901010
+ kFIRNetworkMessageCodeURLSession011 = 901011, // I-NET901011
+ kFIRNetworkMessageCodeURLSession012 = 901012, // I-NET901012
+ kFIRNetworkMessageCodeURLSession013 = 901013, // I-NET901013
+ kFIRNetworkMessageCodeURLSession014 = 901014, // I-NET901014
+ kFIRNetworkMessageCodeURLSession015 = 901015, // I-NET901015
+ kFIRNetworkMessageCodeURLSession016 = 901016, // I-NET901016
+ kFIRNetworkMessageCodeURLSession017 = 901017, // I-NET901017
+ kFIRNetworkMessageCodeURLSession018 = 901018, // I-NET901018
+ // FIRReachabilityChecker.m
+ kFIRNetworkMessageCodeReachabilityChecker000 = 902000, // I-NET902000
+ kFIRNetworkMessageCodeReachabilityChecker001 = 902001, // I-NET902001
+ kFIRNetworkMessageCodeReachabilityChecker002 = 902002, // I-NET902002
+ kFIRNetworkMessageCodeReachabilityChecker003 = 902003, // I-NET902003
+ kFIRNetworkMessageCodeReachabilityChecker004 = 902004, // I-NET902004
+ kFIRNetworkMessageCodeReachabilityChecker005 = 902005, // I-NET902005
+ kFIRNetworkMessageCodeReachabilityChecker006 = 902006, // I-NET902006
+};
diff --git a/Firebase/Core/Private/FIRNetworkURLSession.h b/Firebase/Core/Private/FIRNetworkURLSession.h
new file mode 100644
index 0000000..d146de2
--- /dev/null
+++ b/Firebase/Core/Private/FIRNetworkURLSession.h
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@import Foundation;
+
+#import "FIRNetworkLoggerProtocol.h"
+
+typedef void (^FIRNetworkCompletionHandler)(NSHTTPURLResponse *response, NSData *data,
+ NSError *error);
+typedef void (^FIRNetworkURLSessionCompletionHandler)(NSHTTPURLResponse *response, NSData *data,
+ NSString *sessionID, NSError *error);
+typedef void (^FIRNetworkSystemCompletionHandler)(void);
+
+/// The protocol that uses NSURLSession for iOS >= 7.0 to handle requests and responses.
+@interface FIRNetworkURLSession
+ : NSObject<NSURLSessionDelegate, NSURLSessionTaskDelegate, NSURLSessionDownloadDelegate>
+
+/// Indicates whether the background network is enabled. Default value is NO.
+@property(nonatomic, getter=isBackgroundNetworkEnabled) BOOL backgroundNetworkEnabled;
+
+/// The logger delegate to log message, errors or warnings that occur during the network operations.
+@property(nonatomic, weak) id<FIRNetworkLoggerDelegate> loggerDelegate;
+
+/// Calls the system provided completion handler after the background session is finished.
++ (void)handleEventsForBackgroundURLSessionID:(NSString *)sessionID
+ completionHandler:(FIRNetworkSystemCompletionHandler)completionHandler;
+
+/// Initializes with logger delegate.
+- (instancetype)initWithNetworkLoggerDelegate:(id<FIRNetworkLoggerDelegate>)networkLoggerDelegate
+ NS_DESIGNATED_INITIALIZER;
+
+- (instancetype)init NS_UNAVAILABLE;
+
+/// Sends an asynchronous POST request and calls the provided completion handler when the request
+/// completes or when errors occur, and returns an ID of the session/connection.
+- (NSString *)sessionIDFromAsyncPOSTRequest:(NSURLRequest *)request
+ completionHandler:(FIRNetworkURLSessionCompletionHandler)handler;
+
+/// Sends an asynchronous GET request and calls the provided completion handler when the request
+/// completes or when errors occur, and returns an ID of the session.
+- (NSString *)sessionIDFromAsyncGETRequest:(NSURLRequest *)request
+ completionHandler:(FIRNetworkURLSessionCompletionHandler)handler;
+
+@end
diff --git a/Firebase/Core/Private/FIROptionsInternal.h b/Firebase/Core/Private/FIROptionsInternal.h
new file mode 100644
index 0000000..2b30248
--- /dev/null
+++ b/Firebase/Core/Private/FIROptionsInternal.h
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "../FIROptions.h"
+
+/**
+ * Keys for the strings in the plist file.
+ */
+extern NSString *const kFIRAPIKey;
+extern NSString *const kFIRTrackingID;
+extern NSString *const kFIRGoogleAppID;
+extern NSString *const kFIRClientID;
+extern NSString *const kFIRGCMSenderID;
+extern NSString *const kFIRAndroidClientID;
+extern NSString *const kFIRDatabaseURL;
+extern NSString *const kFIRStorageBucket;
+extern NSString *const kFIRBundleID;
+extern NSString *const kFIRProjectID;
+
+/**
+ * Keys for the plist file name
+ */
+extern NSString *const kServiceInfoFileName;
+
+extern NSString *const kServiceInfoFileType;
+
+/**
+ * This header file exposes the initialization of FIROptions to internal use.
+ */
+@interface FIROptions ()
+
+/**
+ * resetDefaultOptions and initInternalWithOptionsDictionary: are exposed only for unit tests.
+ */
++ (void)resetDefaultOptions;
+
+/**
+ * Initializes the options with dictionary. The above strings are the keys of the dictionary.
+ * This is the designated initializer.
+ */
+- (instancetype)initInternalWithOptionsDictionary:(NSDictionary *)serviceInfoDictionary;
+
+/**
+ * defaultOptions and defaultOptionsDictionary are exposed in order to be used in FIRApp and
+ * other first party services.
+ */
++ (FIROptions *)defaultOptions;
+
++ (NSDictionary *)defaultOptionsDictionary;
+
+/**
+ * Whether or not Analytics Collection was enabled. Analytics Collection is enabled unless
+ * explicitly disabled in GoogleService-Info.plist.
+ */
+@property(nonatomic, readonly) BOOL isAnalyticsCollectionEnabled;
+
+/**
+ * Whether or not Analytics Collection was completely disabled. If YES, then
+ * isAnalyticsCollectionEnabled will be NO.
+ */
+@property(nonatomic, readonly) BOOL isAnalyticsCollectionDeactivated;
+
+/**
+ * The version ID of the client library, e.g. @"1100000".
+ */
+@property(nonatomic, readonly, copy) NSString *libraryVersionID;
+
+/**
+ * The flag indicating whether this object was constructed with the values in the default plist
+ * file.
+ */
+@property(nonatomic) BOOL usingOptionsFromDefaultPlist;
+
+/**
+ * Whether or not Measurement was enabled. Measurement is enabled unless explicitly disabled in
+ * GoogleService-Info.plist.
+ */
+@property(nonatomic, readonly) BOOL isMeasurementEnabled;
+
+/**
+ * Whether or not Analytics was enabled in the developer console.
+ */
+@property(nonatomic, readonly) BOOL isAnalyticsEnabled;
+
+/**
+ * Whether or not SignIn was enabled in the developer console.
+ */
+@property(nonatomic, readonly) BOOL isSignInEnabled;
+
+/**
+ * Whether or not editing is locked. This should occur after FIROptions has been set on a FIRApp.
+ */
+@property(nonatomic, getter=isEditingLocked) BOOL editingLocked;
+
+@end
diff --git a/Firebase/Core/Private/FIRReachabilityChecker+Internal.h b/Firebase/Core/Private/FIRReachabilityChecker+Internal.h
new file mode 100644
index 0000000..f82d103
--- /dev/null
+++ b/Firebase/Core/Private/FIRReachabilityChecker+Internal.h
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRReachabilityChecker.h"
+
+typedef SCNetworkReachabilityRef (*FIRReachabilityCreateWithNameFn)(CFAllocatorRef allocator,
+ const char *host);
+
+typedef Boolean (*FIRReachabilitySetCallbackFn)(SCNetworkReachabilityRef target,
+ SCNetworkReachabilityCallBack callback,
+ SCNetworkReachabilityContext *context);
+typedef Boolean (*FIRReachabilityScheduleWithRunLoopFn)(SCNetworkReachabilityRef target,
+ CFRunLoopRef runLoop,
+ CFStringRef runLoopMode);
+typedef Boolean (*FIRReachabilityUnscheduleFromRunLoopFn)(SCNetworkReachabilityRef target,
+ CFRunLoopRef runLoop,
+ CFStringRef runLoopMode);
+
+typedef void (*FIRReachabilityReleaseFn)(CFTypeRef cf);
+
+struct FIRReachabilityApi {
+ FIRReachabilityCreateWithNameFn createWithNameFn;
+ FIRReachabilitySetCallbackFn setCallbackFn;
+ FIRReachabilityScheduleWithRunLoopFn scheduleWithRunLoopFn;
+ FIRReachabilityUnscheduleFromRunLoopFn unscheduleFromRunLoopFn;
+ FIRReachabilityReleaseFn releaseFn;
+};
+
+@interface FIRReachabilityChecker (Internal)
+
+- (const struct FIRReachabilityApi *)reachabilityApi;
+- (void)setReachabilityApi:(const struct FIRReachabilityApi *)reachabilityApi;
+
+@end
diff --git a/Firebase/Core/Private/FIRReachabilityChecker.h b/Firebase/Core/Private/FIRReachabilityChecker.h
new file mode 100644
index 0000000..105cd3d
--- /dev/null
+++ b/Firebase/Core/Private/FIRReachabilityChecker.h
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@import Foundation;
+@import SystemConfiguration;
+
+/// Reachability Status
+typedef enum {
+ kFIRReachabilityUnknown, ///< Have not yet checked or been notified whether host is reachable.
+ kFIRReachabilityNotReachable, ///< Host is not reachable.
+ kFIRReachabilityViaWifi, ///< Host is reachable via Wifi.
+ kFIRReachabilityViaCellular, ///< Host is reachable via cellular.
+} FIRReachabilityStatus;
+
+const NSString *FIRReachabilityStatusString(FIRReachabilityStatus status);
+
+@class FIRReachabilityChecker;
+@protocol FIRNetworkLoggerDelegate;
+
+/// Google Analytics iOS Reachability Checker.
+@protocol FIRReachabilityDelegate
+@required
+/// Called when network status has changed.
+- (void)reachability:(FIRReachabilityChecker *)reachability
+ statusChanged:(FIRReachabilityStatus)status;
+@end
+
+/// Google Analytics iOS Network Status Checker.
+@interface FIRReachabilityChecker : NSObject
+
+/// The last known reachability status, or FIRReachabilityStatusUnknown if the
+/// checker is not active.
+@property(nonatomic, readonly) FIRReachabilityStatus reachabilityStatus;
+/// The host to which reachability status is to be checked.
+@property(nonatomic, copy, readonly) NSString *host;
+/// The delegate to be notified of reachability status changes.
+@property(nonatomic, weak) id<FIRReachabilityDelegate> reachabilityDelegate;
+/// The delegate to be notified to log messages.
+@property(nonatomic, weak) id<FIRNetworkLoggerDelegate> loggerDelegate;
+/// `YES` if the reachability checker is active, `NO` otherwise.
+@property(nonatomic, readonly) BOOL isActive;
+
+/// Initialize the reachability checker. Note that you must call start to begin checking for and
+/// receiving notifications about network status changes.
+///
+/// @param reachabilityDelegate The delegate to be notified when reachability status to host
+/// changes.
+///
+/// @param loggerDelegate The delegate to send log messages to.
+///
+/// @param host The name of the host.
+///
+- (instancetype)initWithReachabilityDelegate:(id<FIRReachabilityDelegate>)reachabilityDelegate
+ loggerDelegate:(id<FIRNetworkLoggerDelegate>)loggerDelegate
+ withHost:(NSString *)host;
+
+- (instancetype)init NS_UNAVAILABLE;
+
+/// Start checking for reachability to the specified host. This has no effect if the status
+/// checker is already checking for connectivity.
+///
+/// @return `YES` if initiating status checking was successful or the status checking has already
+/// been initiated, `NO` otherwise.
+- (BOOL)start;
+
+/// Stop checking for reachability to the specified host. This has no effect if the status
+/// checker is not checking for connectivity.
+- (void)stop;
+
+@end
diff --git a/Firebase/Core/Private/FIRURLSchemeUtil.h b/Firebase/Core/Private/FIRURLSchemeUtil.h
new file mode 100644
index 0000000..d4fa961
--- /dev/null
+++ b/Firebase/Core/Private/FIRURLSchemeUtil.h
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+/**
+ * Checks whether the URL schemes declared for Google SignIn are valid.
+ *
+ * @param urlSchemes The URL schemes to validate.
+ * @return YES if the schemes are valid; NO otherwise.
+ */
+extern BOOL fir_areURLSchemesValidForGoogleSignIn(NSArray *urlSchemes);
diff --git a/Firebase/Database/Api/FIRDataEventType.h b/Firebase/Database/Api/FIRDataEventType.h
new file mode 100644
index 0000000..fccc98a
--- /dev/null
+++ b/Firebase/Database/Api/FIRDataEventType.h
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef Firebase_FIRDataEventType_h
+#define Firebase_FIRDataEventType_h
+
+#import <Foundation/Foundation.h>
+#import "FIRDatabaseSwiftNameSupport.h"
+
+/**
+ * This enum is the set of events that you can observe at a Firebase Database location.
+ */
+typedef NS_ENUM(NSInteger, FIRDataEventType) {
+ /// A new child node is added to a location.
+ FIRDataEventTypeChildAdded,
+ /// A child node is removed from a location.
+ FIRDataEventTypeChildRemoved,
+ /// A child node at a location changes.
+ FIRDataEventTypeChildChanged,
+ /// A child node moves relative to the other child nodes at a location.
+ FIRDataEventTypeChildMoved,
+ /// Any data changes at a location or, recursively, at any child node.
+ FIRDataEventTypeValue
+} FIR_SWIFT_NAME(DataEventType);
+
+#endif
diff --git a/Firebase/Database/Api/FIRDataSnapshot.h b/Firebase/Database/Api/FIRDataSnapshot.h
new file mode 100644
index 0000000..e615260
--- /dev/null
+++ b/Firebase/Database/Api/FIRDataSnapshot.h
@@ -0,0 +1,148 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+#import "FIRDatabaseSwiftNameSupport.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@class FIRDatabaseReference;
+
+/**
+ * A FIRDataSnapshot contains data from a Firebase Database location. Any time you read
+ * Firebase data, you receive the data as a FIRDataSnapshot.
+ *
+ * FIRDataSnapshots are passed to the blocks you attach with observeEventType:withBlock: or observeSingleEvent:withBlock:.
+ * They are efficiently-generated immutable copies of the data at a Firebase Database location.
+ * They can't be modified and will never change. To modify data at a location,
+ * use a FIRDatabaseReference (e.g. with setValue:).
+ */
+FIR_SWIFT_NAME(DataSnapshot)
+@interface FIRDataSnapshot : NSObject
+
+
+#pragma mark - Navigating and inspecting a snapshot
+
+/**
+ * Gets a FIRDataSnapshot for the location at the specified relative path.
+ * The relative path can either be a simple child key (e.g. 'fred')
+ * or a deeper slash-separated path (e.g. 'fred/name/first'). If the child
+ * location has no data, an empty FIRDataSnapshot is returned.
+ *
+ * @param childPathString A relative path to the location of child data.
+ * @return The FIRDataSnapshot for the child location.
+ */
+- (FIRDataSnapshot *)childSnapshotForPath:(NSString *)childPathString;
+
+
+/**
+ * Return YES if the specified child exists.
+ *
+ * @param childPathString A relative path to the location of a potential child.
+ * @return YES if data exists at the specified childPathString, else NO.
+ */
+- (BOOL) hasChild:(NSString *)childPathString;
+
+
+/**
+ * Return YES if the DataSnapshot has any children.
+ *
+ * @return YES if this snapshot has any children, else NO.
+ */
+- (BOOL) hasChildren;
+
+
+/**
+ * Return YES if the DataSnapshot contains a non-null value.
+ *
+ * @return YES if this snapshot contains a non-null value, else NO.
+ */
+- (BOOL) exists;
+
+
+#pragma mark - Data export
+
+/**
+ * Returns the raw value at this location, coupled with any metadata, such as priority.
+ *
+ * Priorities, where they exist, are accessible under the ".priority" key in instances of NSDictionary.
+ * For leaf locations with priorities, the value will be under the ".value" key.
+ */
+- (id __nullable) valueInExportFormat;
+
+
+#pragma mark - Properties
+
+/**
+ * Returns the contents of this data snapshot as native types.
+ *
+ * Data types returned:
+ * + NSDictionary
+ * + NSArray
+ * + NSNumber (also includes booleans)
+ * + NSString
+ *
+ * @return The data as a native object.
+ */
+@property (strong, readonly, nonatomic, nullable) id value;
+
+
+/**
+ * Gets the number of children for this DataSnapshot.
+ *
+ * @return An integer indicating the number of children.
+ */
+@property (readonly, nonatomic) NSUInteger childrenCount;
+
+
+/**
+ * Gets a FIRDatabaseReference for the location that this data came from.
+ *
+ * @return A FIRDatabaseReference instance for the location of this data.
+ */
+@property (nonatomic, readonly, strong) FIRDatabaseReference * ref;
+
+
+/**
+ * The key of the location that generated this FIRDataSnapshot.
+ *
+ * @return An NSString containing the key for the location of this FIRDataSnapshot.
+ */
+@property (strong, readonly, nonatomic) NSString* key;
+
+
+/**
+ * An iterator for snapshots of the child nodes in this snapshot.
+ * You can use the native for..in syntax:
+ *
+ * for (FIRDataSnapshot* child in snapshot.children) {
+ * ...
+ * }
+ *
+ * @return An NSEnumerator of the children.
+ */
+@property (strong, readonly, nonatomic) NSEnumerator* children;
+
+/**
+ * The priority of the data in this FIRDataSnapshot.
+ *
+ * @return The priority as a string, or nil if no priority was set.
+ */
+@property (strong, readonly, nonatomic, nullable) id priority;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Database/Api/FIRDataSnapshot.m b/Firebase/Database/Api/FIRDataSnapshot.m
new file mode 100644
index 0000000..9559c38
--- /dev/null
+++ b/Firebase/Database/Api/FIRDataSnapshot.m
@@ -0,0 +1,101 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRDataSnapshot.h"
+#import "FIRDataSnapshot_Private.h"
+#import "FChildrenNode.h"
+#import "FValidation.h"
+#import "FTransformedEnumerator.h"
+#import "FIRDatabaseReference.h"
+
+@interface FIRDataSnapshot ()
+@property (nonatomic, strong) FIRDatabaseReference *ref;
+@end
+
+@implementation FIRDataSnapshot
+
+- (id)initWithRef:(FIRDatabaseReference *)ref indexedNode:(FIndexedNode *)node
+{
+ self = [super init];
+ if (self != nil) {
+ self->_ref = ref;
+ self->_node = node;
+ }
+ return self;
+}
+
+- (id) value {
+ return [self.node.node val];
+}
+
+- (id) valueInExportFormat {
+ return [self.node.node valForExport:YES];
+}
+
+- (FIRDataSnapshot *)childSnapshotForPath:(NSString *)childPathString {
+ [FValidation validateFrom:@"child:" validPathString:childPathString];
+ FPath* childPath = [[FPath alloc] initWith:childPathString];
+ FIRDatabaseReference * childRef = [self.ref child:childPathString];
+
+ id<FNode> childNode = [self.node.node getChild:childPath];
+ return [[FIRDataSnapshot alloc] initWithRef:childRef indexedNode:[FIndexedNode indexedNodeWithNode:childNode]];
+}
+
+- (BOOL) hasChild:(NSString *)childPathString {
+ [FValidation validateFrom:@"hasChild:" validPathString:childPathString];
+ FPath* childPath = [[FPath alloc] initWith:childPathString];
+ return ! [[self.node.node getChild:childPath] isEmpty];
+}
+
+- (id) priority {
+ id<FNode> priority = [self.node.node getPriority];
+ return priority.val;
+}
+
+
+- (BOOL) hasChildren {
+ if([self.node.node isLeafNode]) {
+ return false;
+ }
+ else {
+ return ![self.node.node isEmpty];
+ }
+}
+
+- (BOOL) exists {
+ return ![self.node.node isEmpty];
+}
+
+- (NSString *) key {
+ return [self.ref key];
+}
+
+- (NSUInteger) childrenCount {
+ return [self.node.node numChildren];
+}
+
+- (NSEnumerator *) children {
+ return [[FTransformedEnumerator alloc] initWithEnumerator:self.node.childEnumerator andTransform:^id(FNamedNode *node) {
+ FIRDatabaseReference *childRef = [self.ref child:node.name];
+ return [[FIRDataSnapshot alloc] initWithRef:childRef indexedNode:[FIndexedNode indexedNodeWithNode:node.node]];
+ }];
+}
+
+- (NSString *) description {
+ return [NSString stringWithFormat:@"Snap (%@) %@", self.key, self.node.node];
+}
+
+@end
diff --git a/Firebase/Database/Api/FIRDatabase.h b/Firebase/Database/Api/FIRDatabase.h
new file mode 100644
index 0000000..e77ed31
--- /dev/null
+++ b/Firebase/Database/Api/FIRDatabase.h
@@ -0,0 +1,140 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+#import "FIRDatabaseReference.h"
+#import "FIRDatabaseSwiftNameSupport.h"
+
+@class FIRApp;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * The entry point for accessing a Firebase Database. You can get an instance by calling
+ * [FIRDatabase database]. To access a location in the database and read or write data,
+ * use [FIRDatabase reference].
+ */
+FIR_SWIFT_NAME(Database)
+@interface FIRDatabase : NSObject
+
+/**
+ * Gets the instance of FIRDatabase for the default FIRApp.
+ *
+ * @return A FIRDatabase instance.
+ */
++ (FIRDatabase *) database FIR_SWIFT_NAME(database());
+
+/**
+ * Gets an instance of FIRDatabase for a specific FIRApp.
+ *
+ * @param app The FIRApp to get a FIRDatabase for.
+ * @return A FIRDatabase instance.
+ */
++ (FIRDatabase *) databaseForApp:(FIRApp*)app FIR_SWIFT_NAME(database(app:));
+
+/** The FIRApp instance to which this FIRDatabase belongs. */
+@property (weak, readonly, nonatomic) FIRApp *app;
+
+/**
+ * Gets a FIRDatabaseReference for the root of your Firebase Database.
+ */
+- (FIRDatabaseReference *) reference;
+
+/**
+ * Gets a FIRDatabaseReference for the provided path.
+ *
+ * @param path Path to a location in your Firebase Database.
+ * @return A FIRDatabaseReference pointing to the specified path.
+ */
+- (FIRDatabaseReference *) referenceWithPath:(NSString *)path;
+
+/**
+ * Gets a FIRDatabaseReference for the provided URL. The URL must be a URL to a path
+ * within this Firebase Database. To create a FIRDatabaseReference to a different database,
+ * create a FIRApp} with a FIROptions object configured with the appropriate database URL.
+ *
+ * @param databaseUrl A URL to a path within your database.
+ * @return A FIRDatabaseReference for the provided URL.
+*/
+- (FIRDatabaseReference *) referenceFromURL:(NSString *)databaseUrl;
+
+/**
+ * The Firebase Database client automatically queues writes and sends them to the server at the earliest opportunity,
+ * depending on network connectivity. In some cases (e.g. offline usage) there may be a large number of writes
+ * waiting to be sent. Calling this method will purge all outstanding writes so they are abandoned.
+ *
+ * All writes will be purged, including transactions and onDisconnect writes. The writes will
+ * be rolled back locally, perhaps triggering events for affected event listeners, and the client will not
+ * (re-)send them to the Firebase Database backend.
+ */
+- (void)purgeOutstandingWrites;
+
+/**
+ * Shuts down our connection to the Firebase Database backend until goOnline is called.
+ */
+- (void)goOffline;
+
+/**
+ * Resumes our connection to the Firebase Database backend after a previous goOffline call.
+ */
+- (void)goOnline;
+
+/**
+ * The Firebase Database client will cache synchronized data and keep track of all writes you've
+ * initiated while your application is running. It seamlessly handles intermittent network
+ * connections and re-sends write operations when the network connection is restored.
+ *
+ * However by default your write operations and cached data are only stored in-memory and will
+ * be lost when your app restarts. By setting this value to `YES`, the data will be persisted
+ * to on-device (disk) storage and will thus be available again when the app is restarted
+ * (even when there is no network connectivity at that time). Note that this property must be
+ * set before creating your first Database reference and only needs to be called once per
+ * application.
+ *
+ */
+@property (nonatomic) BOOL persistenceEnabled FIR_SWIFT_NAME(isPersistenceEnabled);
+
+/**
+ * By default the Firebase Database client will use up to 10MB of disk space to cache data. If the cache grows beyond
+ * this size, the client will start removing data that hasn't been recently used. If you find that your application
+ * caches too little or too much data, call this method to change the cache size. This property must be set before
+ * creating your first FIRDatabaseReference and only needs to be called once per application.
+ *
+ * Note that the specified cache size is only an approximation and the size on disk may temporarily exceed it
+ * at times. Cache sizes smaller than 1 MB or greater than 100 MB are not supported.
+ */
+@property (nonatomic) NSUInteger persistenceCacheSizeBytes;
+
+/**
+ * Sets the dispatch queue on which all events are raised. The default queue is the main queue.
+ *
+ * Note that this must be set before creating your first Database reference.
+ */
+@property (nonatomic, strong) dispatch_queue_t callbackQueue;
+
+/**
+ * Enables verbose diagnostic logging.
+ *
+ * @param enabled YES to enable logging, NO to disable.
+ */
++ (void) setLoggingEnabled:(BOOL)enabled;
+
+/** Retrieve the Firebase Database SDK version. */
++ (NSString *) sdkVersion;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Database/Api/FIRDatabase.m b/Firebase/Database/Api/FIRDatabase.m
new file mode 100644
index 0000000..124b463
--- /dev/null
+++ b/Firebase/Database/Api/FIRDatabase.m
@@ -0,0 +1,268 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+#import "FIRDatabase.h"
+#import "FIRDatabase_Private.h"
+#import "FIRDatabaseQuery_Private.h"
+#import "FRepoManager.h"
+#import "FValidation.h"
+#import "FIRDatabaseConfig_Private.h"
+#import "FRepoInfo.h"
+#import "FIRDatabaseConfig.h"
+#import "FIRDatabaseReference_Private.h"
+
+/**
+ * This is a hack that defines all the methods we need from FIRApp/Options. At runtime we use reflection to get the
+ * default FIRApp instance if we need it. Since protocols don't carry any runtime information and selectors
+ * are invoked by name we can write code against this protocol as long as the method signatures don't change.
+ *
+ * TODO: Consider weak-linking the actual Firebase/Core framework or something.
+ */
+
+extern NSString *const kFIRDefaultAppName;
+
+@protocol FIROptionsLike <NSObject>
+@property(nonatomic, readonly, copy) NSString *databaseURL;
+@end
+
+@protocol FIRAppLike <NSObject>
+@property(nonatomic, readonly) id<FIROptionsLike> options;
+@property(nonatomic, copy, readonly) NSString *name;
+@end
+
+@interface FIRDatabase ()
+@property (nonatomic, strong) FRepoInfo *repoInfo;
+@property (nonatomic, strong) FIRDatabaseConfig *config;
+@property (nonatomic, strong) FRepo *repo;
+@end
+
+@implementation FIRDatabase
+
+// The STR and STR_EXPAND macro allow a numeric version passed to he compiler driver
+// with a -D to be treated as a string instead of an invalid floating point value.
+#define STR(x) STR_EXPAND(x)
+#define STR_EXPAND(x) #x
+static const char *FIREBASE_SEMVER = (const char *)STR(FIRDatabase_VERSION);
+
+/**
+ * A static NSMutableDictionary of FirebaseApp names to FirebaseDatabase instance. To ensure thread-
+ * safety, it should only be accessed in databaseForApp, which is synchronized.
+ *
+ * TODO: This serves a duplicate purpose as RepoManager. We should clean up.
+ * TODO: We should maybe be conscious of leaks and make this a weak map or similar
+ * but we have a lot of work to do to allow FirebaseDatabase/Repo etc. to be GC'd.
+ */
++ (NSMutableDictionary *)instances {
+ static dispatch_once_t pred = 0;
+ static NSMutableDictionary *instances;
+ dispatch_once(&pred, ^{
+ instances = [NSMutableDictionary dictionary];
+ });
+ return instances;
+}
+
++ (FIRDatabase *)database {
+ id<FIRAppLike> app = [FIRDatabase getDefaultApp];
+ if (app == nil) {
+ [NSException raise:@"FIRAppNotConfigured" format:@"Failed to get default FIRDatabase instance. Must call FIRApp.configure() before using FIRDatabase."];
+ }
+ return [FIRDatabase databaseForApp:(FIRApp*)app];
+}
+
++ (FIRDatabase *)databaseForApp:(id)app {
+ if (app == nil) {
+ [NSException raise:@"InvalidFIRApp" format:@"nil FIRApp instance passed to databaseForApp."];
+ }
+ NSMutableDictionary *instances = [self instances];
+ @synchronized (instances) {
+ id<FIRAppLike> appLike = (id<FIRAppLike>)app;
+ FIRDatabase *database = instances[appLike.name];
+ if (!database) {
+ NSString *databaseUrl = appLike.options.databaseURL;
+ if (databaseUrl == nil) {
+ [NSException raise:@"MissingDatabaseURL" format:@"Failed to get FIRDatabase instance: FIRApp object has no "
+ "databaseURL in its FirebaseOptions object."];
+ }
+
+ FParsedUrl *parsedUrl = [FUtilities parseUrl:databaseUrl];
+ if (![parsedUrl.path isEmpty]) {
+ [NSException raise:@"InvalidDatabaseURL" format:@"Configured Database URL '%@' is invalid. It should "
+ "point to the root of a Firebase Database but it includes a path: %@",
+ databaseUrl, [parsedUrl.path toString]];
+ }
+
+ id<FAuthTokenProvider> authTokenProvider = [FAuthTokenProvider authTokenProviderForApp:appLike];
+
+ // If this is the default app, don't set the session persistence key so that we use our
+ // default ("default") instead of the FIRApp default ("[DEFAULT]") so that we
+ // preserve the default location used by the legacy Firebase SDK.
+ NSString *sessionIdentifier = @"default";
+ if (![appLike.name isEqualToString:kFIRDefaultAppName]) {
+ sessionIdentifier = appLike.name;
+ }
+
+ FIRDatabaseConfig *config = [[FIRDatabaseConfig alloc] initWithSessionIdentifier:sessionIdentifier
+ authTokenProvider:authTokenProvider];
+ database = [[FIRDatabase alloc] initWithApp:appLike repoInfo:parsedUrl.repoInfo config:config];
+ instances[appLike.name] = database;
+ }
+
+ return database;
+ }
+}
+
++ (NSString *) buildVersion {
+ // TODO: Restore git hash when build moves back to git
+ return [NSString stringWithFormat:@"%s_%s", FIREBASE_SEMVER, __DATE__];
+}
+
++ (FIRDatabase *)createDatabaseForTests:(FRepoInfo *)repoInfo config:(FIRDatabaseConfig *)config {
+ FIRDatabase *db = [[FIRDatabase alloc] initWithApp:nil repoInfo:repoInfo config:config];
+ [db ensureRepo];
+ return db;
+}
+
+
++ (NSString *) sdkVersion {
+ return [NSString stringWithUTF8String:FIREBASE_SEMVER];
+}
+
++ (void) setLoggingEnabled:(BOOL)enabled {
+ [FUtilities setLoggingEnabled:enabled];
+ FFLog(@"I-RDB024001", @"BUILD Version: %@", [FIRDatabase buildVersion]);
+}
+
+
+- (id)initWithApp:(id <FIRAppLike>)appLike repoInfo:(FRepoInfo *)info config:(FIRDatabaseConfig *)config {
+ self = [super init];
+ if (self != nil) {
+ self->_repoInfo = info;
+ self->_config = config;
+ self->_app = (FIRApp*) appLike;
+ }
+ return self;
+}
+
+- (FIRDatabaseReference *)reference {
+ [self ensureRepo];
+
+ return [[FIRDatabaseReference alloc] initWithRepo:self.repo path:[FPath empty]];
+}
+
+- (FIRDatabaseReference *)referenceWithPath:(NSString *)path {
+ [self ensureRepo];
+
+ [FValidation validateFrom:@"referenceWithPath" validRootPathString:path];
+ FPath *childPath = [[FPath alloc] initWith:path];
+ return [[FIRDatabaseReference alloc] initWithRepo:self.repo path:childPath];
+}
+
+- (FIRDatabaseReference *)referenceFromURL:(NSString *)databaseUrl {
+ [self ensureRepo];
+
+ if (databaseUrl == nil) {
+ [NSException raise:@"InvalidDatabaseURL" format:@"Invalid nil url passed to referenceFromURL:"];
+ }
+ FParsedUrl *parsedUrl = [FUtilities parseUrl:databaseUrl];
+ [FValidation validateFrom:@"referenceFromURL:" validURL:parsedUrl];
+ if (![parsedUrl.repoInfo.host isEqualToString:_repoInfo.host]) {
+ [NSException raise:@"InvalidDatabaseURL" format:@"Invalid URL (%@) passed to getReference(). URL was expected "
+ "to match configured Database URL: %@", databaseUrl, [self reference].URL];
+ }
+ return [[FIRDatabaseReference alloc] initWithRepo:self.repo path:parsedUrl.path];
+}
+
+
+- (void)purgeOutstandingWrites {
+ [self ensureRepo];
+
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ [self.repo purgeOutstandingWrites];
+ });
+}
+
+- (void)goOnline {
+ [self ensureRepo];
+
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ [self.repo resume];
+ });
+}
+
+
+- (void)goOffline {
+ [self ensureRepo];
+
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ [self.repo interrupt];
+ });
+}
+
++ (id<FIRAppLike>) getDefaultApp {
+ Class appClass = NSClassFromString(@"FIRApp");
+ if (appClass == nil) {
+ [NSException raise:@"FailedToFindFIRApp" format:@"Failed to find FIRApp class."];
+ return nil;
+ } else {
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wundeclared-selector"
+ return [appClass performSelector:@selector(defaultApp)];
+#pragma clang diagnostic pop
+ }
+}
+
+- (void)setPersistenceEnabled:(BOOL)persistenceEnabled {
+ [self assertUnfrozen:@"setPersistenceEnabled"];
+ self->_config.persistenceEnabled = persistenceEnabled;
+}
+
+- (BOOL)persistenceEnabled {
+ return self->_config.persistenceEnabled;
+}
+
+- (void)setPersistenceCacheSizeBytes:(NSUInteger)persistenceCacheSizeBytes {
+ [self assertUnfrozen:@"setPersistenceCacheSizeBytes"];
+ self->_config.persistenceCacheSizeBytes = persistenceCacheSizeBytes;
+}
+
+- (NSUInteger)persistenceCacheSizeBytes {
+ return self->_config.persistenceCacheSizeBytes;
+}
+
+- (void)setCallbackQueue:(dispatch_queue_t)callbackQueue {
+ [self assertUnfrozen:@"setCallbackQueue"];
+ self->_config.callbackQueue = callbackQueue;
+}
+
+- (dispatch_queue_t)callbackQueue {
+ return self->_config.callbackQueue;
+}
+
+- (void) assertUnfrozen:(NSString*)methodName {
+ if (self.repo != nil) {
+ [NSException raise:@"FIRDatabaseAlreadyInUse" format:@"Calls to %@ must be made before any other usage of "
+ "FIRDatabase instance.", methodName];
+ }
+}
+
+- (void) ensureRepo {
+ if (self.repo == nil) {
+ self.repo = [FRepoManager createRepo:self.repoInfo config:self.config database:self];
+ }
+}
+
+@end
diff --git a/Firebase/Database/Api/FIRDatabaseConfig.h b/Firebase/Database/Api/FIRDatabaseConfig.h
new file mode 100644
index 0000000..d41f3a8
--- /dev/null
+++ b/Firebase/Database/Api/FIRDatabaseConfig.h
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+@protocol FAuthTokenProvider;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * TODO: Merge FIRDatabaseConfig into FIRDatabase.
+ */
+@interface FIRDatabaseConfig : NSObject
+
+- (id)initWithSessionIdentifier:(NSString *)identifier authTokenProvider:(id<FAuthTokenProvider>)authTokenProvider;
+
+/**
+ * By default the Firebase Database client will keep data in memory while your application is running, but not
+ * when it is restarted. By setting this value to YES, the data will be persisted to on-device (disk)
+ * storage and will thus be available again when the app is restarted (even when there is no network
+ * connectivity at that time). Note that this property must be set before creating your first FIRDatabaseReference
+ * and only needs to be called once per application.
+ *
+ * If your app uses Firebase Authentication, the client will automatically persist the user's authentication
+ * token across restarts, even without persistence enabled. But if the auth token expired while offline and
+ * you've enabled persistence, the client will pause write operations until you successfully re-authenticate
+ * (or explicitly unauthenticate) to prevent your writes from being sent unauthenticated and failing due to
+ * security rules.
+ */
+@property (nonatomic) BOOL persistenceEnabled;
+
+/**
+ * By default the Firebase Database client will use up to 10MB of disk space to cache data. If the cache grows beyond this size,
+ * the client will start removing data that hasn't been recently used. If you find that your application caches too
+ * little or too much data, call this method to change the cache size. This property must be set before creating
+ * your first FIRDatabaseReference and only needs to be called once per application.
+ *
+ * Note that the specified cache size is only an approximation and the size on disk may temporarily exceed it
+ * at times.
+ */
+@property (nonatomic) NSUInteger persistenceCacheSizeBytes;
+
+/**
+ * Sets the dispatch queue on which all events are raised. The default queue is the main queue.
+ */
+@property (nonatomic, strong) dispatch_queue_t callbackQueue;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Database/Api/FIRDatabaseConfig.m b/Firebase/Database/Api/FIRDatabaseConfig.m
new file mode 100644
index 0000000..f4639f9
--- /dev/null
+++ b/Firebase/Database/Api/FIRDatabaseConfig.m
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRApp.h"
+#import "FIRDatabaseConfig.h"
+#import "FIRDatabaseConfig_Private.h"
+#import "FIRNoopAuthTokenProvider.h"
+#import "FAuthTokenProvider.h"
+
+@interface FIRDatabaseConfig (Private)
+
+@property (nonatomic, strong, readwrite) NSString *sessionIdentifier;
+
+@end
+
+@implementation FIRDatabaseConfig
+
+- (id)init {
+ [NSException raise:NSInvalidArgumentException format:@"Can't create config objects!"];
+ return nil;
+}
+
+- (id)initWithSessionIdentifier:(NSString *)identifier authTokenProvider:(id<FAuthTokenProvider>)authTokenProvider {
+ self = [super init];
+ if (self != nil) {
+ self->_sessionIdentifier = identifier;
+ self->_callbackQueue = dispatch_get_main_queue();
+ self->_persistenceCacheSizeBytes = 10*1024*1024; // Default cache size is 10MB
+ self->_authTokenProvider = authTokenProvider;
+ }
+ return self;
+}
+
+- (void)assertUnfrozen {
+ if (self.isFrozen) {
+ [NSException raise:NSGenericException format:@"Can't modify config objects after they are in use for FIRDatabaseReferences."];
+ }
+}
+
+- (void)setAuthTokenProvider:(id<FAuthTokenProvider>)authTokenProvider {
+ [self assertUnfrozen];
+ self->_authTokenProvider = authTokenProvider;
+}
+
+- (void)setPersistenceEnabled:(BOOL)persistenceEnabled {
+ [self assertUnfrozen];
+ self->_persistenceEnabled = persistenceEnabled;
+}
+
+- (void)setPersistenceCacheSizeBytes:(NSUInteger)persistenceCacheSizeBytes {
+ [self assertUnfrozen];
+ // Can't be less than 1MB
+ if (persistenceCacheSizeBytes < 1024*1024) {
+ [NSException raise:NSInvalidArgumentException format:@"The minimum cache size must be at least 1MB"];
+ }
+ if (persistenceCacheSizeBytes > 100*1024*1024) {
+ [NSException raise:NSInvalidArgumentException format:@"Firebase Database currently doesn't support a cache size larger than 100MB"];
+ }
+ self->_persistenceCacheSizeBytes = persistenceCacheSizeBytes;
+}
+
+- (void)setCallbackQueue:(dispatch_queue_t)callbackQueue {
+ [self assertUnfrozen];
+ self->_callbackQueue = callbackQueue;
+}
+
+- (void)freeze {
+ self->_isFrozen = YES;
+}
+
+// TODO: Only used for tests. Migrate to FIRDatabase and remove.
++ (FIRDatabaseConfig *)defaultConfig {
+ static dispatch_once_t onceToken;
+ static FIRDatabaseConfig *defaultConfig;
+ dispatch_once(&onceToken, ^{
+ defaultConfig = [FIRDatabaseConfig configForName:@"default"];
+ });
+ return defaultConfig;
+}
+
+// TODO: This is only used for tests. We should fix them to go through FIRDatabase and remove
+// this method and the sessionsConfigs dictionary (FIRDatabase automatically creates one config per app).
++ (FIRDatabaseConfig *)configForName:(NSString *)name {
+ NSRegularExpression *expression = [NSRegularExpression regularExpressionWithPattern:@"^[a-zA-Z0-9-_]+$" options:0 error:nil];
+ if ([expression numberOfMatchesInString:name options:0 range:NSMakeRange(0, name.length)] == 0) {
+ [NSException raise:NSInvalidArgumentException format:@"Name can only contain [a-zA-Z0-9-_]"];
+ }
+
+ static dispatch_once_t onceToken;
+ static NSMutableDictionary *sessionConfigs;
+ dispatch_once(&onceToken, ^{
+ sessionConfigs = [NSMutableDictionary dictionary];
+ });
+ @synchronized(sessionConfigs) {
+ if (!sessionConfigs[name]) {
+ id<FAuthTokenProvider> authTokenProvider = [FAuthTokenProvider authTokenProviderForApp:[FIRApp defaultApp]];
+ sessionConfigs[name] = [[FIRDatabaseConfig alloc] initWithSessionIdentifier:name
+ authTokenProvider:authTokenProvider];
+ }
+ return sessionConfigs[name];
+ }
+}
+
+@end
diff --git a/Firebase/Database/Api/FIRDatabaseQuery.h b/Firebase/Database/Api/FIRDatabaseQuery.h
new file mode 100644
index 0000000..be4ad27
--- /dev/null
+++ b/Firebase/Database/Api/FIRDatabaseQuery.h
@@ -0,0 +1,315 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+#import "FIRDatabaseSwiftNameSupport.h"
+#import "FIRDataEventType.h"
+#import "FIRDataSnapshot.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * A FIRDatabaseHandle is used to identify listeners of Firebase Database events. These handles
+ * are returned by observeEventType: and and can later be passed to removeObserverWithHandle: to
+ * stop receiving updates.
+ */
+typedef NSUInteger FIRDatabaseHandle FIR_SWIFT_NAME(DatabaseHandle);
+
+/**
+ * A FIRDatabaseQuery instance represents a query over the data at a particular location.
+ *
+ * You create one by calling one of the query methods (queryOrderedByChild:, queryStartingAtValue:, etc.)
+ * on a FIRDatabaseReference. The query methods can be chained to further specify the data you are interested in
+ * observing
+ */
+FIR_SWIFT_NAME(DatabaseQuery)
+@interface FIRDatabaseQuery : NSObject
+
+
+#pragma mark - Attach observers to read data
+
+/**
+ * observeEventType:withBlock: is used to listen for data changes at a particular location.
+ * This is the primary way to read data from the Firebase Database. Your block will be triggered
+ * for the initial data and again whenever the data changes.
+ *
+ * Use removeObserverWithHandle: to stop receiving updates.
+ *
+ * @param eventType The type of event to listen for.
+ * @param block The block that should be called with initial data and updates. It is passed the data as a FIRDataSnapshot.
+ * @return A handle used to unregister this block later using removeObserverWithHandle:
+ */
+- (FIRDatabaseHandle)observeEventType:(FIRDataEventType)eventType withBlock:(void (^)(FIRDataSnapshot *snapshot))block;
+
+
+/**
+ * observeEventType:andPreviousSiblingKeyWithBlock: is used to listen for data changes at a particular location.
+ * This is the primary way to read data from the Firebase Database. Your block will be triggered
+ * for the initial data and again whenever the data changes. In addition, for FIRDataEventTypeChildAdded, FIRDataEventTypeChildMoved, and
+ * FIRDataEventTypeChildChanged events, your block will be passed the key of the previous node by priority order.
+ *
+ * Use removeObserverWithHandle: to stop receiving updates.
+ *
+ * @param eventType The type of event to listen for.
+ * @param block The block that should be called with initial data and updates. It is passed the data as a FIRDataSnapshot
+ * and the previous child's key.
+ * @return A handle used to unregister this block later using removeObserverWithHandle:
+ */
+- (FIRDatabaseHandle)observeEventType:(FIRDataEventType)eventType andPreviousSiblingKeyWithBlock:(void (^)(FIRDataSnapshot *snapshot, NSString *__nullable prevKey))block;
+
+
+/**
+ * observeEventType:withBlock: is used to listen for data changes at a particular location.
+ * This is the primary way to read data from the Firebase Database. Your block will be triggered
+ * for the initial data and again whenever the data changes.
+ *
+ * The cancelBlock will be called if you will no longer receive new events due to no longer having permission.
+ *
+ * Use removeObserverWithHandle: to stop receiving updates.
+ *
+ * @param eventType The type of event to listen for.
+ * @param block The block that should be called with initial data and updates. It is passed the data as a FIRDataSnapshot.
+ * @param cancelBlock The block that should be called if this client no longer has permission to receive these events
+ * @return A handle used to unregister this block later using removeObserverWithHandle:
+ */
+- (FIRDatabaseHandle)observeEventType:(FIRDataEventType)eventType withBlock:(void (^)(FIRDataSnapshot *snapshot))block withCancelBlock:(nullable void (^)(NSError* error))cancelBlock;
+
+
+/**
+ * observeEventType:andPreviousSiblingKeyWithBlock: is used to listen for data changes at a particular location.
+ * This is the primary way to read data from the Firebase Database. Your block will be triggered
+ * for the initial data and again whenever the data changes. In addition, for FIRDataEventTypeChildAdded, FIRDataEventTypeChildMoved, and
+ * FIRDataEventTypeChildChanged events, your block will be passed the key of the previous node by priority order.
+ *
+ * The cancelBlock will be called if you will no longer receive new events due to no longer having permission.
+ *
+ * Use removeObserverWithHandle: to stop receiving updates.
+ *
+ * @param eventType The type of event to listen for.
+ * @param block The block that should be called with initial data and updates. It is passed the data as a FIRDataSnapshot
+ * and the previous child's key.
+ * @param cancelBlock The block that should be called if this client no longer has permission to receive these events
+ * @return A handle used to unregister this block later using removeObserverWithHandle:
+ */
+- (FIRDatabaseHandle)observeEventType:(FIRDataEventType)eventType andPreviousSiblingKeyWithBlock:(void (^)(FIRDataSnapshot *snapshot, NSString *__nullable prevKey))block withCancelBlock:(nullable void (^)(NSError* error))cancelBlock;
+
+
+/**
+ * This is equivalent to observeEventType:withBlock:, except the block is immediately canceled after the initial data is returned.
+ *
+ * @param eventType The type of event to listen for.
+ * @param block The block that should be called. It is passed the data as a FIRDataSnapshot.
+ */
+- (void)observeSingleEventOfType:(FIRDataEventType)eventType withBlock:(void (^)(FIRDataSnapshot *snapshot))block;
+
+
+/**
+ * This is equivalent to observeEventType:withBlock:, except the block is immediately canceled after the initial data is returned. In addition, for FIRDataEventTypeChildAdded, FIRDataEventTypeChildMoved, and
+ * FIRDataEventTypeChildChanged events, your block will be passed the key of the previous node by priority order.
+ *
+ * @param eventType The type of event to listen for.
+ * @param block The block that should be called. It is passed the data as a FIRDataSnapshot and the previous child's key.
+ */
+- (void)observeSingleEventOfType:(FIRDataEventType)eventType andPreviousSiblingKeyWithBlock:(void (^)(FIRDataSnapshot *snapshot, NSString *__nullable prevKey))block;
+
+
+/**
+ * This is equivalent to observeEventType:withBlock:, except the block is immediately canceled after the initial data is returned.
+ *
+ * The cancelBlock will be called if you do not have permission to read data at this location.
+ *
+ * @param eventType The type of event to listen for.
+ * @param block The block that should be called. It is passed the data as a FIRDataSnapshot.
+ * @param cancelBlock The block that will be called if you don't have permission to access this data
+ */
+- (void)observeSingleEventOfType:(FIRDataEventType)eventType withBlock:(void (^)(FIRDataSnapshot *snapshot))block withCancelBlock:(nullable void (^)(NSError* error))cancelBlock;
+
+
+/**
+ * This is equivalent to observeEventType:withBlock:, except the block is immediately canceled after the initial data is returned. In addition, for FIRDataEventTypeChildAdded, FIRDataEventTypeChildMoved, and
+ * FIRDataEventTypeChildChanged events, your block will be passed the key of the previous node by priority order.
+ *
+ * The cancelBlock will be called if you do not have permission to read data at this location.
+ *
+ * @param eventType The type of event to listen for.
+ * @param block The block that should be called. It is passed the data as a FIRDataSnapshot and the previous child's key.
+ * @param cancelBlock The block that will be called if you don't have permission to access this data
+ */
+- (void)observeSingleEventOfType:(FIRDataEventType)eventType andPreviousSiblingKeyWithBlock:(void (^)(FIRDataSnapshot *snapshot, NSString *__nullable prevKey))block withCancelBlock:(nullable void (^)(NSError* error))cancelBlock;
+
+
+#pragma mark - Detaching observers
+
+/**
+ * Detach a block previously attached with observeEventType:withBlock:.
+ *
+ * @param handle The handle returned by the call to observeEventType:withBlock: which we are trying to remove.
+ */
+- (void) removeObserverWithHandle:(FIRDatabaseHandle)handle;
+
+
+/**
+ * Detach all blocks previously attached to this Firebase Database location with observeEventType:withBlock:
+ */
+- (void) removeAllObservers;
+
+/**
+ * By calling `keepSynced:YES` on a location, the data for that location will automatically be downloaded and
+ * kept in sync, even when no listeners are attached for that location. Additionally, while a location is kept
+ * synced, it will not be evicted from the persistent disk cache.
+ *
+ * @param keepSynced Pass YES to keep this location synchronized, pass NO to stop synchronization.
+*/
+ - (void) keepSynced:(BOOL)keepSynced;
+
+
+#pragma mark - Querying and limiting
+
+/**
+* queryLimitedToFirst: is used to generate a reference to a limited view of the data at this location.
+* The FIRDatabaseQuery instance returned by queryLimitedToFirst: will respond to at most the first limit child nodes.
+*
+* @param limit The upper bound, inclusive, for the number of child nodes to receive events for
+* @return A FIRDatabaseQuery instance, limited to at most limit child nodes.
+*/
+- (FIRDatabaseQuery *)queryLimitedToFirst:(NSUInteger)limit;
+
+
+/**
+* queryLimitedToLast: is used to generate a reference to a limited view of the data at this location.
+* The FIRDatabaseQuery instance returned by queryLimitedToLast: will respond to at most the last limit child nodes.
+*
+* @param limit The upper bound, inclusive, for the number of child nodes to receive events for
+* @return A FIRDatabaseQuery instance, limited to at most limit child nodes.
+*/
+- (FIRDatabaseQuery *)queryLimitedToLast:(NSUInteger)limit;
+
+/**
+ * queryOrderBy: is used to generate a reference to a view of the data that's been sorted by the values of
+ * a particular child key. This method is intended to be used in combination with queryStartingAtValue:,
+ * queryEndingAtValue:, or queryEqualToValue:.
+ *
+ * @param key The child key to use in ordering data visible to the returned FIRDatabaseQuery
+ * @return A FIRDatabaseQuery instance, ordered by the values of the specified child key.
+*/
+- (FIRDatabaseQuery *)queryOrderedByChild:(NSString *)key;
+
+/**
+ * queryOrderedByKey: is used to generate a reference to a view of the data that's been sorted by child key.
+ * This method is intended to be used in combination with queryStartingAtValue:, queryEndingAtValue:,
+ * or queryEqualToValue:.
+ *
+ * @return A FIRDatabaseQuery instance, ordered by child keys.
+ */
+- (FIRDatabaseQuery *) queryOrderedByKey;
+
+/**
+ * queryOrderedByValue: is used to generate a reference to a view of the data that's been sorted by child value.
+ * This method is intended to be used in combination with queryStartingAtValue:, queryEndingAtValue:,
+ * or queryEqualToValue:.
+ *
+ * @return A FIRDatabaseQuery instance, ordered by child value.
+ */
+- (FIRDatabaseQuery *) queryOrderedByValue;
+
+/**
+ * queryOrderedByPriority: is used to generate a reference to a view of the data that's been sorted by child
+ * priority. This method is intended to be used in combination with queryStartingAtValue:, queryEndingAtValue:,
+ * or queryEqualToValue:.
+ *
+ * @return A FIRDatabaseQuery instance, ordered by child priorities.
+ */
+- (FIRDatabaseQuery *) queryOrderedByPriority;
+
+/**
+ * queryStartingAtValue: is used to generate a reference to a limited view of the data at this location.
+ * The FIRDatabaseQuery instance returned by queryStartingAtValue: will respond to events at nodes with a value
+ * greater than or equal to startValue.
+ *
+ * @param startValue The lower bound, inclusive, for the value of data visible to the returned FIRDatabaseQuery
+ * @return A FIRDatabaseQuery instance, limited to data with value greater than or equal to startValue
+ */
+- (FIRDatabaseQuery *)queryStartingAtValue:(nullable id)startValue;
+
+/**
+ * queryStartingAtValue:childKey: is used to generate a reference to a limited view of the data at this location.
+ * The FIRDatabaseQuery instance returned by queryStartingAtValue:childKey will respond to events at nodes with a value
+ * greater than startValue, or equal to startValue and with a key greater than or equal to childKey. This is most
+ * useful when implementing pagination in a case where multiple nodes can match the startValue.
+ *
+ * @param startValue The lower bound, inclusive, for the value of data visible to the returned FIRDatabaseQuery
+ * @param childKey The lower bound, inclusive, for the key of nodes with value equal to startValue
+ * @return A FIRDatabaseQuery instance, limited to data with value greater than or equal to startValue
+ */
+- (FIRDatabaseQuery *)queryStartingAtValue:(nullable id)startValue childKey:(nullable NSString *)childKey;
+
+/**
+ * queryEndingAtValue: is used to generate a reference to a limited view of the data at this location.
+ * The FIRDatabaseQuery instance returned by queryEndingAtValue: will respond to events at nodes with a value
+ * less than or equal to endValue.
+ *
+ * @param endValue The upper bound, inclusive, for the value of data visible to the returned FIRDatabaseQuery
+ * @return A FIRDatabaseQuery instance, limited to data with value less than or equal to endValue
+ */
+- (FIRDatabaseQuery *)queryEndingAtValue:(nullable id)endValue;
+
+/**
+ * queryEndingAtValue:childKey: is used to generate a reference to a limited view of the data at this location.
+ * The FIRDatabaseQuery instance returned by queryEndingAtValue:childKey will respond to events at nodes with a value
+ * less than endValue, or equal to endValue and with a key less than or equal to childKey. This is most useful when
+ * implementing pagination in a case where multiple nodes can match the endValue.
+ *
+ * @param endValue The upper bound, inclusive, for the value of data visible to the returned FIRDatabaseQuery
+ * @param childKey The upper bound, inclusive, for the key of nodes with value equal to endValue
+ * @return A FIRDatabaseQuery instance, limited to data with value less than or equal to endValue
+ */
+- (FIRDatabaseQuery *)queryEndingAtValue:(nullable id)endValue childKey:(nullable NSString *)childKey;
+
+/**
+ * queryEqualToValue: is used to generate a reference to a limited view of the data at this location.
+ * The FIRDatabaseQuery instance returned by queryEqualToValue: will respond to events at nodes with a value equal
+ * to the supplied argument.
+ *
+ * @param value The value that the data returned by this FIRDatabaseQuery will have
+ * @return A FIRDatabaseQuery instance, limited to data with the supplied value.
+ */
+- (FIRDatabaseQuery *)queryEqualToValue:(nullable id)value;
+
+/**
+ * queryEqualToValue:childKey: is used to generate a reference to a limited view of the data at this location.
+ * The FIRDatabaseQuery instance returned by queryEqualToValue:childKey will respond to events at nodes with a value
+ * equal to the supplied argument and with their key equal to childKey. There will be at most one node that matches
+ * because child keys are unique.
+ *
+ * @param value The value that the data returned by this FIRDatabaseQuery will have
+ * @param childKey The name of nodes with the right value
+ * @return A FIRDatabaseQuery instance, limited to data with the supplied value and the key.
+ */
+- (FIRDatabaseQuery *)queryEqualToValue:(nullable id)value childKey:(nullable NSString *)childKey;
+
+
+#pragma mark - Properties
+
+/**
+* Gets a FIRDatabaseReference for the location of this query.
+*
+* @return A FIRDatabaseReference for the location of this query.
+*/
+@property (nonatomic, readonly, strong) FIRDatabaseReference * ref;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Database/Api/FIRDatabaseQuery.m b/Firebase/Database/Api/FIRDatabaseQuery.m
new file mode 100644
index 0000000..bcb1733
--- /dev/null
+++ b/Firebase/Database/Api/FIRDatabaseQuery.m
@@ -0,0 +1,525 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRDatabaseQuery.h"
+#import "FIRDatabaseQuery_Private.h"
+#import "FValidation.h"
+#import "FQueryParams.h"
+#import "FQuerySpec.h"
+#import "FValueEventRegistration.h"
+#import "FChildEventRegistration.h"
+#import "FPath.h"
+#import "FKeyIndex.h"
+#import "FPathIndex.h"
+#import "FPriorityIndex.h"
+#import "FValueIndex.h"
+#import "FLeafNode.h"
+#import "FSnapshotUtilities.h"
+#import "FConstants.h"
+
+@implementation FIRDatabaseQuery
+
+@synthesize repo;
+@synthesize path;
+@synthesize queryParams;
+
+#define INVALID_QUERY_PARAM_ERROR @"InvalidQueryParameter"
+
+
++ (dispatch_queue_t)sharedQueue
+{
+ // We use this shared queue across all of the FQueries so things happen FIFO (as opposed to dispatch_get_global_queue(0, 0) which is concurrent)
+ static dispatch_once_t pred;
+ static dispatch_queue_t sharedDispatchQueue;
+
+ dispatch_once(&pred, ^{
+ sharedDispatchQueue = dispatch_queue_create("FirebaseWorker", NULL);
+ });
+
+ return sharedDispatchQueue;
+}
+
+- (id) initWithRepo:(FRepo *)theRepo path:(FPath *)thePath {
+ return [self initWithRepo:theRepo path:thePath params:nil orderByCalled:NO priorityMethodCalled:NO];
+}
+
+- (id) initWithRepo:(FRepo *)theRepo
+ path:(FPath *)thePath
+ params:(FQueryParams *)theParams
+ orderByCalled:(BOOL)orderByCalled
+priorityMethodCalled:(BOOL)priorityMethodCalled {
+ self = [super init];
+ if (self) {
+ self.repo = theRepo;
+ self.path = thePath;
+ if (!theParams) {
+ theParams = [FQueryParams defaultInstance];
+ }
+ if (![theParams isValid]) {
+ @throw [[NSException alloc] initWithName:@"InvalidArgumentError" reason:@"Queries are limited to two constraints" userInfo:nil];
+ }
+ self.queryParams = theParams;
+ self.orderByCalled = orderByCalled;
+ self.priorityMethodCalled = priorityMethodCalled;
+ }
+ return self;
+}
+
+- (FQuerySpec *)querySpec {
+ return [[FQuerySpec alloc] initWithPath:self.path params:self.queryParams];
+}
+
+- (void)validateQueryEndpointsForParams:(FQueryParams *)params {
+ if ([params.index isEqual:[FKeyIndex keyIndex]]) {
+ if ([params hasStart]) {
+ if (params.indexStartKey != [FUtilities minName]) {
+ [NSException raise:INVALID_QUERY_PARAM_ERROR format:@"Can't use queryStartingAtValue:childKey: or queryEqualTo:andChildKey: in combination with queryOrderedByKey"];
+ }
+ if (![params.indexStartValue.val isKindOfClass:[NSString class]]) {
+ [NSException raise:INVALID_QUERY_PARAM_ERROR format:@"Can't use queryStartingAtValue: with other types than string in combination with queryOrderedByKey"];
+ }
+ }
+ if ([params hasEnd]) {
+ if (params.indexEndKey != [FUtilities maxName]) {
+ [NSException raise:INVALID_QUERY_PARAM_ERROR format:@"Can't use queryEndingAtValue:childKey: or queryEqualToValue:childKey: in combination with queryOrderedByKey"];
+ }
+ if (![params.indexEndValue.val isKindOfClass:[NSString class]]) {
+ [NSException raise:INVALID_QUERY_PARAM_ERROR format:@"Can't use queryEndingAtValue: with other types than string in combination with queryOrderedByKey"];
+ }
+ }
+ } else if ([params.index isEqual:[FPriorityIndex priorityIndex]]) {
+ if (([params hasStart] && ![FValidation validatePriorityValue:params.indexStartValue.val]) ||
+ ([params hasEnd] && ![FValidation validatePriorityValue:params.indexEndValue.val])) {
+ [NSException raise:INVALID_QUERY_PARAM_ERROR format:@"When using queryOrderedByPriority, values provided to queryStartingAtValue:, queryEndingAtValue:, or queryEqualToValue: must be valid priorities."];
+ }
+ }
+}
+
+- (void)validateEqualToCall {
+ if ([self.queryParams hasStart]) {
+ [NSException raise:INVALID_QUERY_PARAM_ERROR format:@"Cannot combine queryEqualToValue: and queryStartingAtValue:"];
+ }
+ if ([self.queryParams hasEnd]) {
+ [NSException raise:INVALID_QUERY_PARAM_ERROR format:@"Cannot combine queryEqualToValue: and queryEndingAtValue:"];
+ }
+}
+
+- (void)validateNoPreviousOrderByCalled {
+ if (self.orderByCalled) {
+ [NSException raise:INVALID_QUERY_PARAM_ERROR format:@"Cannot use multiple queryOrderedBy calls!"];
+ }
+}
+
+- (void)validateIndexValueType:(id)type fromMethod:(NSString *)method {
+ if (type != nil &&
+ ![type isKindOfClass:[NSNumber class]] &&
+ ![type isKindOfClass:[NSString class]] &&
+ ![type isKindOfClass:[NSNull class]]) {
+ [NSException raise:INVALID_QUERY_PARAM_ERROR format:@"You can only pass nil, NSString or NSNumber to %@", method];
+ }
+}
+
+- (FIRDatabaseQuery *)queryStartingAtValue:(id)startValue {
+ return [self queryStartingAtInternal:startValue childKey:nil from:@"queryStartingAtValue:" priorityMethod:NO];
+}
+
+- (FIRDatabaseQuery *)queryStartingAtValue:(id)startValue childKey:(NSString *)childKey {
+ if ([self.queryParams.index isEqual:[FKeyIndex keyIndex]]) {
+ @throw [[NSException alloc] initWithName:INVALID_QUERY_PARAM_ERROR
+ reason:@"You must use queryStartingAtValue: instead of queryStartingAtValue:childKey: when using queryOrderedByKey:"
+ userInfo:nil];
+ }
+ return [self queryStartingAtInternal:startValue
+ childKey:childKey
+ from:@"queryStartingAtValue:childKey:"
+ priorityMethod:NO];
+}
+
+- (FIRDatabaseQuery *)queryStartingAtInternal:(id<FNode>)startValue
+ childKey:(NSString *)childKey
+ from:(NSString *)methodName
+ priorityMethod:(BOOL)priorityMethod {
+ [self validateIndexValueType:startValue fromMethod:methodName];
+ if (childKey != nil) {
+ [FValidation validateFrom:methodName validKey:childKey];
+ }
+ if ([self.queryParams hasStart]) {
+ [NSException raise:INVALID_QUERY_PARAM_ERROR
+ format:@"Can't call %@ after queryStartingAtValue or queryEqualToValue was previously called", methodName];
+ }
+ id<FNode> startNode = [FSnapshotUtilities nodeFrom:startValue];
+ FQueryParams* params = [self.queryParams startAt:startNode childKey:childKey];
+ [self validateQueryEndpointsForParams:params];
+ return [[FIRDatabaseQuery alloc] initWithRepo:self.repo
+ path:self.path
+ params:params
+ orderByCalled:self.orderByCalled
+ priorityMethodCalled:priorityMethod || self.priorityMethodCalled];
+}
+
+- (FIRDatabaseQuery *)queryEndingAtValue:(id)endValue {
+ return [self queryEndingAtInternal:endValue
+ childKey:nil
+ from:@"queryEndingAtValue:"
+ priorityMethod:NO];
+}
+
+- (FIRDatabaseQuery *)queryEndingAtValue:(id)endValue childKey:(NSString *)childKey {
+ if ([self.queryParams.index isEqual:[FKeyIndex keyIndex]]) {
+ @throw [[NSException alloc] initWithName:INVALID_QUERY_PARAM_ERROR
+ reason:@"You must use queryEndingAtValue: instead of queryEndingAtValue:childKey: when using queryOrderedByKey:"
+ userInfo:nil];
+ }
+
+ return [self queryEndingAtInternal:endValue
+ childKey:childKey
+ from:@"queryEndingAtValue:childKey:"
+ priorityMethod:NO];
+}
+
+- (FIRDatabaseQuery *)queryEndingAtInternal:(id)endValue
+ childKey:(NSString *)childKey
+ from:(NSString *)methodName
+ priorityMethod:(BOOL)priorityMethod {
+ [self validateIndexValueType:endValue fromMethod:methodName];
+ if (childKey != nil) {
+ [FValidation validateFrom:methodName validKey:childKey];
+ }
+ if ([self.queryParams hasEnd]) {
+ [NSException raise:INVALID_QUERY_PARAM_ERROR
+ format:@"Can't call %@ after queryEndingAtValue or queryEqualToValue was previously called", methodName];
+ }
+ id<FNode> endNode = [FSnapshotUtilities nodeFrom:endValue];
+ FQueryParams* params = [self.queryParams endAt:endNode childKey:childKey];
+ [self validateQueryEndpointsForParams:params];
+ return [[FIRDatabaseQuery alloc] initWithRepo:self.repo
+ path:self.path
+ params:params
+ orderByCalled:self.orderByCalled
+ priorityMethodCalled:priorityMethod || self.priorityMethodCalled];
+}
+
+- (FIRDatabaseQuery *)queryEqualToValue:(id)value {
+ return [self queryEqualToInternal:value childKey:nil from:@"queryEqualToValue:" priorityMethod:NO];
+}
+
+- (FIRDatabaseQuery *)queryEqualToValue:(id)value childKey:(NSString *)childKey {
+ if ([self.queryParams.index isEqual:[FKeyIndex keyIndex]]) {
+ @throw [[NSException alloc] initWithName:INVALID_QUERY_PARAM_ERROR
+ reason:@"You must use queryEqualToValue: instead of queryEqualTo:childKey: when using queryOrderedByKey:"
+ userInfo:nil];
+ }
+ return [self queryEqualToInternal:value childKey:childKey from:@"queryEqualToValue:childKey:" priorityMethod:NO];
+}
+
+- (FIRDatabaseQuery *)queryEqualToInternal:(id)value
+ childKey:(NSString *)childKey
+ from:(NSString *)methodName
+ priorityMethod:(BOOL)priorityMethod {
+ [self validateIndexValueType:value fromMethod:methodName];
+ if (childKey != nil) {
+ [FValidation validateFrom:methodName validKey:childKey];
+ }
+ if ([self.queryParams hasEnd] || [self.queryParams hasStart]) {
+ [NSException raise:INVALID_QUERY_PARAM_ERROR
+ format:@"Can't call %@ after queryStartingAtValue, queryEndingAtValue or queryEqualToValue was previously called", methodName];
+ }
+ id<FNode> node = [FSnapshotUtilities nodeFrom:value];
+ FQueryParams* params = [[self.queryParams startAt:node childKey:childKey] endAt:node childKey:childKey];
+ [self validateQueryEndpointsForParams:params];
+ return [[FIRDatabaseQuery alloc] initWithRepo:self.repo
+ path:self.path
+ params:params
+ orderByCalled:self.orderByCalled
+ priorityMethodCalled:priorityMethod || self.priorityMethodCalled];
+}
+
+- (void)validateLimitRange:(NSUInteger)limit
+{
+ // No need to check for negative ranges, since limit is unsigned
+ if (limit == 0) {
+ [NSException raise:INVALID_QUERY_PARAM_ERROR format:@"Limit can't be zero"];
+ }
+ if (limit >= 1l<<31) {
+ [NSException raise:INVALID_QUERY_PARAM_ERROR format:@"Limit must be less than 2,147,483,648"];
+ }
+}
+
+- (FIRDatabaseQuery *)queryLimitedToFirst:(NSUInteger)limit {
+ if (self.queryParams.limitSet) {
+ [NSException raise:INVALID_QUERY_PARAM_ERROR format:@"Can't call queryLimitedToFirst: if a limit was previously set"];
+ }
+ [self validateLimitRange:limit];
+ FQueryParams* params = [self.queryParams limitToFirst:limit];
+ return [[FIRDatabaseQuery alloc] initWithRepo:self.repo
+ path:self.path
+ params:params
+ orderByCalled:self.orderByCalled
+ priorityMethodCalled:self.priorityMethodCalled];
+}
+
+- (FIRDatabaseQuery *)queryLimitedToLast:(NSUInteger)limit {
+ if (self.queryParams.limitSet) {
+ [NSException raise:INVALID_QUERY_PARAM_ERROR format:@"Can't call queryLimitedToLast: if a limit was previously set"];
+ }
+ [self validateLimitRange:limit];
+ FQueryParams* params = [self.queryParams limitToLast:limit];
+ return [[FIRDatabaseQuery alloc] initWithRepo:self.repo
+ path:self.path
+ params:params
+ orderByCalled:self.orderByCalled
+ priorityMethodCalled:self.priorityMethodCalled];
+}
+
+- (FIRDatabaseQuery *)queryOrderedByChild:(NSString *)indexPathString {
+ if ([indexPathString isEqualToString:@"$key"] || [indexPathString isEqualToString:@".key"]) {
+ @throw [[NSException alloc] initWithName:INVALID_QUERY_PARAM_ERROR
+ reason:[NSString stringWithFormat:@"(queryOrderedByChild:) %@ is invalid. Use queryOrderedByKey: instead.", indexPathString]
+ userInfo:nil];
+ } else if ([indexPathString isEqualToString:@"$priority"] || [indexPathString isEqualToString:@".priority"]) {
+ @throw [[NSException alloc] initWithName:INVALID_QUERY_PARAM_ERROR
+ reason:[NSString stringWithFormat:@"(queryOrderedByChild:) %@ is invalid. Use queryOrderedByPriority: instead.", indexPathString]
+ userInfo:nil];
+ } else if ([indexPathString isEqualToString:@"$value"] || [indexPathString isEqualToString:@".value"]) {
+ @throw [[NSException alloc] initWithName:INVALID_QUERY_PARAM_ERROR
+ reason:[NSString stringWithFormat:@"(queryOrderedByChild:) %@ is invalid. Use queryOrderedByValue: instead.", indexPathString]
+ userInfo:nil];
+ }
+ [self validateNoPreviousOrderByCalled];
+
+ [FValidation validateFrom:@"queryOrderedByChild:" validPathString:indexPathString];
+ FPath *indexPath = [FPath pathWithString:indexPathString];
+ if (indexPath.isEmpty) {
+ @throw [[NSException alloc] initWithName:INVALID_QUERY_PARAM_ERROR
+ reason:[NSString stringWithFormat:@"(queryOrderedByChild:) with an empty path is invalid. Use queryOrderedByValue: instead."]
+ userInfo:nil];
+ }
+ id<FIndex> index = [[FPathIndex alloc] initWithPath:indexPath];
+
+ FQueryParams *params = [self.queryParams orderBy:index];
+ [self validateQueryEndpointsForParams:params];
+ return [[FIRDatabaseQuery alloc] initWithRepo:self.repo
+ path:self.path
+ params:params
+ orderByCalled:YES
+ priorityMethodCalled:self.priorityMethodCalled];
+}
+
+- (FIRDatabaseQuery *) queryOrderedByKey {
+ [self validateNoPreviousOrderByCalled];
+ FQueryParams *params = [self.queryParams orderBy:[FKeyIndex keyIndex]];
+ [self validateQueryEndpointsForParams:params];
+ return [[FIRDatabaseQuery alloc] initWithRepo:self.repo
+ path:self.path
+ params:params
+ orderByCalled:YES
+ priorityMethodCalled:self.priorityMethodCalled];
+}
+
+- (FIRDatabaseQuery *) queryOrderedByValue {
+ [self validateNoPreviousOrderByCalled];
+ FQueryParams *params = [self.queryParams orderBy:[FValueIndex valueIndex]];
+ return [[FIRDatabaseQuery alloc] initWithRepo:self.repo
+ path:self.path
+ params:params
+ orderByCalled:YES
+ priorityMethodCalled:self.priorityMethodCalled];
+}
+
+- (FIRDatabaseQuery *) queryOrderedByPriority {
+ [self validateNoPreviousOrderByCalled];
+ FQueryParams *params = [self.queryParams orderBy:[FPriorityIndex priorityIndex]];
+ return [[FIRDatabaseQuery alloc] initWithRepo:self.repo
+ path:self.path
+ params:params
+ orderByCalled:YES
+ priorityMethodCalled:self.priorityMethodCalled];
+}
+
+- (FIRDatabaseHandle)observeEventType:(FIRDataEventType)eventType withBlock:(void (^)(FIRDataSnapshot *))block {
+ [FValidation validateFrom:@"observeEventType:withBlock:" knownEventType:eventType];
+ return [self observeEventType:eventType withBlock:block withCancelBlock:nil];
+}
+
+
+- (FIRDatabaseHandle)observeEventType:(FIRDataEventType)eventType andPreviousSiblingKeyWithBlock:(fbt_void_datasnapshot_nsstring)block {
+ [FValidation validateFrom:@"observeEventType:andPreviousSiblingKeyWithBlock:" knownEventType:eventType];
+ return [self observeEventType:eventType andPreviousSiblingKeyWithBlock:block withCancelBlock:nil];
+}
+
+
+- (FIRDatabaseHandle)observeEventType:(FIRDataEventType)eventType withBlock:(fbt_void_datasnapshot)block withCancelBlock:(fbt_void_nserror)cancelBlock {
+ [FValidation validateFrom:@"observeEventType:withBlock:withCancelBlock:" knownEventType:eventType];
+
+ if (eventType == FIRDataEventTypeValue) {
+ // Handle FIRDataEventTypeValue specially because they shouldn't have prevName callbacks
+ NSUInteger handle = [[FUtilities LUIDGenerator] integerValue];
+ [self observeValueEventWithHandle:handle withBlock:block cancelCallback:cancelBlock];
+ return handle;
+ } else {
+ // Wrap up the userCallback so we can treat everything as a callback that has a prevName
+ fbt_void_datasnapshot userCallback = [block copy];
+ return [self observeEventType:eventType andPreviousSiblingKeyWithBlock:^(FIRDataSnapshot *snapshot, NSString *prevName) {
+ if (userCallback != nil) {
+ userCallback(snapshot);
+ }
+ } withCancelBlock:cancelBlock];
+ }
+}
+
+- (FIRDatabaseHandle)observeEventType:(FIRDataEventType)eventType andPreviousSiblingKeyWithBlock:(fbt_void_datasnapshot_nsstring)block withCancelBlock:(fbt_void_nserror)cancelBlock {
+ [FValidation validateFrom:@"observeEventType:andPreviousSiblingKeyWithBlock:withCancelBlock:" knownEventType:eventType];
+
+
+ if (eventType == FIRDataEventTypeValue) {
+ // TODO: This gets hit by observeSingleEventOfType. Need to fix.
+ /*
+ @throw [[NSException alloc] initWithName:@"InvalidEventTypeForObserver"
+ reason:@"(observeEventType:andPreviousSiblingKeyWithBlock:withCancelBlock:) Cannot use observeEventType:andPreviousSiblingKeyWithBlock:withCancelBlock: with FIRDataEventTypeValue. Use observeEventType:withBlock:withCancelBlock: instead."
+ userInfo:nil];
+ */
+ }
+
+ NSUInteger handle = [[FUtilities LUIDGenerator] integerValue];
+ NSDictionary *callbacks = @{[NSNumber numberWithInteger:eventType]: [block copy]};
+ [self observeChildEventWithHandle:handle withCallbacks:callbacks cancelCallback:cancelBlock];
+
+ return handle;
+}
+
+// If we want to distinguish between value event listeners and child event listeners, like in the Java client, we can
+// consider exporting this. If we do, add argument validation. Otherwise, arguments are validated in the public-facing
+// portions of the API. Also, move the FIRDatabaseHandle logic.
+- (void)observeValueEventWithHandle:(FIRDatabaseHandle)handle withBlock:(fbt_void_datasnapshot)block cancelCallback:(fbt_void_nserror)cancelBlock {
+ // Note that we don't need to copy the callbacks here, FEventRegistration callback properties set to copy
+ FValueEventRegistration *registration = [[FValueEventRegistration alloc] initWithRepo:self.repo
+ handle:handle
+ callback:block
+ cancelCallback:cancelBlock];
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ [self.repo addEventRegistration:registration forQuery:self.querySpec];
+ });
+}
+
+// Note: as with the above method, we may wish to expose this at some point.
+- (void)observeChildEventWithHandle:(FIRDatabaseHandle)handle withCallbacks:(NSDictionary *)callbacks cancelCallback:(fbt_void_nserror)cancelBlock {
+ // Note that we don't need to copy the callbacks here, FEventRegistration callback properties set to copy
+ FChildEventRegistration *registration = [[FChildEventRegistration alloc] initWithRepo:self.repo
+ handle:handle
+ callbacks:callbacks
+ cancelCallback:cancelBlock];
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ [self.repo addEventRegistration:registration forQuery:self.querySpec];
+ });
+}
+
+
+- (void) removeObserverWithHandle:(FIRDatabaseHandle)handle {
+ FValueEventRegistration *event = [[FValueEventRegistration alloc] initWithRepo:self.repo
+ handle:handle
+ callback:nil
+ cancelCallback:nil];
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ [self.repo removeEventRegistration:event forQuery:self.querySpec];
+ });
+}
+
+
+- (void) removeAllObservers {
+ [self removeObserverWithHandle:NSNotFound];
+}
+
+- (void)keepSynced:(BOOL)keepSynced {
+ if ([self.path.getFront isEqualToString:kDotInfoPrefix]) {
+ [NSException raise:NSInvalidArgumentException format:@"Can't keep query on .info tree synced (this already is the case)."];
+ }
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ [self.repo keepQuery:self.querySpec synced:keepSynced];
+ });
+}
+
+- (void)observeSingleEventOfType:(FIRDataEventType)eventType withBlock:(fbt_void_datasnapshot)block {
+
+ [self observeSingleEventOfType:eventType withBlock:block withCancelBlock:nil];
+}
+
+
+- (void)observeSingleEventOfType:(FIRDataEventType)eventType andPreviousSiblingKeyWithBlock:(fbt_void_datasnapshot_nsstring)block {
+
+ [self observeSingleEventOfType:eventType andPreviousSiblingKeyWithBlock:block withCancelBlock:nil];
+}
+
+
+- (void)observeSingleEventOfType:(FIRDataEventType)eventType withBlock:(fbt_void_datasnapshot)block withCancelBlock:(fbt_void_nserror)cancelBlock {
+
+ // XXX: user reported memory leak in method
+
+ // "When you copy a block, any references to other blocks from within that block are copied if necessary—an entire tree may be copied (from the top). If you have block variables and you reference a block from within the block, that block will be copied."
+ // http://developer.apple.com/library/ios/#documentation/cocoa/Conceptual/Blocks/Articles/bxVariables.html#//apple_ref/doc/uid/TP40007502-CH6-SW1
+ // So... we don't need to do this since inside the on: we copy this block off the stack to the heap.
+ // __block fbt_void_datasnapshot userCallback = [callback copy];
+
+ [self observeSingleEventOfType:eventType andPreviousSiblingKeyWithBlock:^(FIRDataSnapshot *snapshot, NSString *prevName) {
+ if (block != nil) {
+ block(snapshot);
+ }
+ } withCancelBlock:cancelBlock];
+}
+
+/**
+* Attaches a listener, waits for the first event, and then removes the listener
+*/
+- (void)observeSingleEventOfType:(FIRDataEventType)eventType andPreviousSiblingKeyWithBlock:(fbt_void_datasnapshot_nsstring)block withCancelBlock:(fbt_void_nserror)cancelBlock {
+
+ // XXX: user reported memory leak in method
+
+ // "When you copy a block, any references to other blocks from within that block are copied if necessary—an entire tree may be copied (from the top). If you have block variables and you reference a block from within the block, that block will be copied."
+ // http://developer.apple.com/library/ios/#documentation/cocoa/Conceptual/Blocks/Articles/bxVariables.html#//apple_ref/doc/uid/TP40007502-CH6-SW1
+ // So... we don't need to do this since inside the on: we copy this block off the stack to the heap.
+ // __block fbt_void_datasnapshot userCallback = [callback copy];
+
+ __block FIRDatabaseHandle handle;
+ __block BOOL firstCall = YES;
+
+ fbt_void_datasnapshot_nsstring callback = [block copy];
+ fbt_void_datasnapshot_nsstring wrappedCallback = ^(FIRDataSnapshot *snap, NSString* prevName) {
+ if (firstCall) {
+ firstCall = NO;
+ [self removeObserverWithHandle:handle];
+ callback(snap, prevName);
+ }
+ };
+
+ fbt_void_nserror cancelCallback = [cancelBlock copy];
+ handle = [self observeEventType:eventType andPreviousSiblingKeyWithBlock:wrappedCallback withCancelBlock:^(NSError* error){
+
+ [self removeObserverWithHandle:handle];
+
+ if (cancelCallback) {
+ cancelCallback(error);
+ }
+ }];
+}
+
+- (NSString *) description {
+ return [NSString stringWithFormat:@"(%@ %@)", self.path, self.queryParams.description];
+}
+
+- (FIRDatabaseReference *) ref {
+ return [[FIRDatabaseReference alloc] initWithRepo:self.repo path:self.path];
+}
+
+@end
diff --git a/Firebase/Database/Api/FIRDatabaseSwiftNameSupport.h b/Firebase/Database/Api/FIRDatabaseSwiftNameSupport.h
new file mode 100644
index 0000000..529adf4
--- /dev/null
+++ b/Firebase/Database/Api/FIRDatabaseSwiftNameSupport.h
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef FIR_SWIFT_NAME
+
+#import <Foundation/Foundation.h>
+
+// NS_SWIFT_NAME can only translate factory methods before the iOS 9.3 SDK.
+// // Wrap it in our own macro if it's a non-compatible SDK.
+#ifdef __IPHONE_9_3
+#define FIR_SWIFT_NAME(X) NS_SWIFT_NAME(X)
+#else
+#define FIR_SWIFT_NAME(X) // Intentionally blank.
+#endif // #ifdef __IPHONE_9_3
+
+#endif // FIR_SWIFT_NAME \ No newline at end of file
diff --git a/Firebase/Database/Api/FIRMutableData.h b/Firebase/Database/Api/FIRMutableData.h
new file mode 100644
index 0000000..5c26024
--- /dev/null
+++ b/Firebase/Database/Api/FIRMutableData.h
@@ -0,0 +1,130 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+#import "FIRDatabaseSwiftNameSupport.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * A FIRMutableData instance is populated with data from a Firebase Database location.
+ * When you are using runTransactionBlock:, you will be given an instance containing the current
+ * data at that location. Your block will be responsible for updating that instance to the data
+ * you wish to save at that location, and then returning using [FIRTransactionResult successWithValue:].
+ *
+ * To modify the data, set its value property to any of the native types support by Firebase Database:
+ *
+ * + NSNumber (includes BOOL)
+ * + NSDictionary
+ * + NSArray
+ * + NSString
+ * + nil / NSNull to remove the data
+ *
+ * Note that changes made to a child FIRMutableData instance will be visible to the parent.
+ */
+FIR_SWIFT_NAME(MutableData)
+@interface FIRMutableData : NSObject
+
+
+#pragma mark - Inspecting and navigating the data
+
+
+/**
+ * Returns boolean indicating whether this mutable data has children.
+ *
+ * @return YES if this data contains child nodes.
+ */
+- (BOOL) hasChildren;
+
+
+/**
+ * Indicates whether this mutable data has a child at the given path.
+ *
+ * @param path A path string, consisting either of a single segment, like 'child', or multiple segments, 'a/deeper/child'
+ * @return YES if this data contains a child at the specified relative path
+ */
+- (BOOL) hasChildAtPath:(NSString *)path;
+
+
+/**
+ * Used to obtain a FIRMutableData instance that encapsulates the data at the given relative path.
+ * Note that changes made to the child will be visible to the parent.
+ *
+ * @param path A path string, consisting either of a single segment, like 'child', or multiple segments, 'a/deeper/child'
+ * @return A FIRMutableData instance containing the data at the given path
+ */
+- (FIRMutableData *)childDataByAppendingPath:(NSString *)path;
+
+
+#pragma mark - Properties
+
+
+/**
+ * To modify the data contained by this instance of FIRMutableData, set this to any of the native types supported by Firebase Database:
+ *
+ * + NSNumber (includes BOOL)
+ * + NSDictionary
+ * + NSArray
+ * + NSString
+ * + nil / NSNull to remove the data
+ *
+ * Note that setting this value will override the priority at this location.
+ *
+ * @return The current data at this location as a native object
+ */
+@property (strong, nonatomic, nullable) id value;
+
+
+/**
+ * Set this property to update the priority of the data at this location. Can be set to the following types:
+ *
+ * + NSNumber
+ * + NSString
+ * + nil / NSNull to remove the priority
+ *
+ * @return The priority of the data at this location
+ */
+@property (strong, nonatomic, nullable) id priority;
+
+
+/**
+ * @return The number of child nodes at this location
+ */
+@property (readonly, nonatomic) NSUInteger childrenCount;
+
+
+/**
+ * Used to iterate over the children at this location. You can use the native for .. in syntax:
+ *
+ * for (FIRMutableData* child in data.children) {
+ * ...
+ * }
+ *
+ * Note that this enumerator operates on an immutable copy of the child list. So, you can modify the instance
+ * during iteration, but the new additions will not be visible until you get a new enumerator.
+ */
+@property (readonly, nonatomic, strong) NSEnumerator* children;
+
+
+/**
+ * @return The key name of this node, or nil if it is the top-most location
+ */
+@property (readonly, nonatomic, strong, nullable) NSString* key;
+
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Database/Api/FIRMutableData.m b/Firebase/Database/Api/FIRMutableData.m
new file mode 100644
index 0000000..7e10dcd
--- /dev/null
+++ b/Firebase/Database/Api/FIRMutableData.m
@@ -0,0 +1,134 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRMutableData.h"
+#import "FIRMutableData_Private.h"
+#import "FSnapshotHolder.h"
+#import "FSnapshotUtilities.h"
+#import "FChildrenNode.h"
+#import "FTransformedEnumerator.h"
+#import "FNamedNode.h"
+#import "FIndexedNode.h"
+
+@interface FIRMutableData ()
+
+- (id) initWithPrefixPath:(FPath *)path andSnapshotHolder:(FSnapshotHolder *)snapshotHolder;
+
+@property (strong, nonatomic) FSnapshotHolder* data;
+@property (strong, nonatomic) FPath* prefixPath;
+
+@end
+
+@implementation FIRMutableData
+
+@synthesize data;
+@synthesize prefixPath;
+
+- (id) initWithNode:(id<FNode>)node {
+ FSnapshotHolder* holder = [[FSnapshotHolder alloc] init];
+ FPath* path = [FPath empty];
+ [holder updateSnapshot:path withNewSnapshot:node];
+ return [self initWithPrefixPath:path andSnapshotHolder:holder];
+}
+
+- (id) initWithPrefixPath:(FPath *)path andSnapshotHolder:(FSnapshotHolder *)snapshotHolder {
+ self = [super init];
+ if (self) {
+ self.prefixPath = path;
+ self.data = snapshotHolder;
+ }
+ return self;
+}
+
+- (FIRMutableData *)childDataByAppendingPath:(NSString *)path {
+ FPath* wholePath = [self.prefixPath childFromString:path];
+ return [[FIRMutableData alloc] initWithPrefixPath:wholePath andSnapshotHolder:self.data];
+}
+
+- (FIRMutableData *) parent {
+ if ([self.prefixPath isEmpty]) {
+ return nil;
+ } else {
+ FPath* path = [self.prefixPath parent];
+ return [[FIRMutableData alloc] initWithPrefixPath:path andSnapshotHolder:self.data];
+ }
+}
+
+- (void) setValue:(id)aValue {
+ id<FNode> node = [FSnapshotUtilities nodeFrom:aValue withValidationFrom:@"setValue:"];
+ [self.data updateSnapshot:self.prefixPath withNewSnapshot:node];
+}
+
+- (void) setPriority:(id)aPriority {
+ id<FNode> node = [self.data getNode:self.prefixPath];
+ id<FNode> pri = [FSnapshotUtilities nodeFrom:aPriority];
+ node = [node updatePriority:pri];
+ [self.data updateSnapshot:self.prefixPath withNewSnapshot:node];
+}
+
+- (id) value {
+ return [[self.data getNode:self.prefixPath] val];
+}
+
+- (id) priority {
+ return [[[self.data getNode:self.prefixPath] getPriority] val];
+}
+
+- (BOOL) hasChildren {
+ id<FNode> node = [self.data getNode:self.prefixPath];
+ return ![node isLeafNode] && ![(FChildrenNode*)node isEmpty];
+}
+
+- (BOOL) hasChildAtPath:(NSString *)path {
+ id<FNode> node = [self.data getNode:self.prefixPath];
+ FPath* childPath = [[FPath alloc] initWith:path];
+ return ![[node getChild:childPath] isEmpty];
+}
+
+- (NSUInteger) childrenCount {
+ return [[self.data getNode:self.prefixPath] numChildren];
+}
+
+- (NSString *) key {
+ return [self.prefixPath getBack];
+}
+
+- (id<FNode>) nodeValue {
+ return [self.data getNode:self.prefixPath];
+}
+
+- (NSEnumerator *) children {
+ FIndexedNode *indexedNode = [FIndexedNode indexedNodeWithNode:self.nodeValue];
+ return [[FTransformedEnumerator alloc] initWithEnumerator:[indexedNode childEnumerator] andTransform:^id(FNamedNode *node) {
+ FPath* childPath = [self.prefixPath childFromString:node.name];
+ FIRMutableData * childData = [[FIRMutableData alloc] initWithPrefixPath:childPath andSnapshotHolder:self.data];
+ return childData;
+ }];
+}
+
+- (BOOL) isEqualToData:(FIRMutableData *)other {
+ return self.data == other.data && [[self.prefixPath description] isEqualToString:[other.prefixPath description]];
+}
+
+- (NSString *) description {
+ if (self.key == nil) {
+ return [NSString stringWithFormat:@"FIRMutableData (top-most transaction) %@ %@", self.key, self.value];
+ } else {
+ return [NSString stringWithFormat:@"FIRMutableData (%@) %@", self.key, self.value];
+ }
+}
+
+@end
diff --git a/Firebase/Database/Api/FIRServerValue.h b/Firebase/Database/Api/FIRServerValue.h
new file mode 100644
index 0000000..f5eadd5
--- /dev/null
+++ b/Firebase/Database/Api/FIRServerValue.h
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRDatabaseSwiftNameSupport.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * Placeholder values you may write into Firebase Database as a value or priority
+ * that will automatically be populated by the Firebase Database server.
+ */
+FIR_SWIFT_NAME(ServerValue)
+@interface FIRServerValue : NSObject
+
+/**
+ * Placeholder value for the number of milliseconds since the Unix epoch
+ */
++ (NSDictionary *) timestamp;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Database/Api/FIRServerValue.m b/Firebase/Database/Api/FIRServerValue.m
new file mode 100644
index 0000000..14bb745
--- /dev/null
+++ b/Firebase/Database/Api/FIRServerValue.m
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRDatabaseReference.h"
+#import "FIRServerValue.h"
+
+@implementation FIRServerValue
+
++ (NSDictionary *) timestamp {
+ static NSDictionary *timestamp = nil;
+ if (timestamp == nil) {
+ timestamp = @{ @".sv": @"timestamp" };
+ }
+ return timestamp;
+}
+
+@end
diff --git a/Firebase/Database/Api/FIRTransactionResult.h b/Firebase/Database/Api/FIRTransactionResult.h
new file mode 100644
index 0000000..3c2d39a
--- /dev/null
+++ b/Firebase/Database/Api/FIRTransactionResult.h
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+#import "FIRDatabaseSwiftNameSupport.h"
+#import "FIRMutableData.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * Used for runTransactionBlock:. An FIRTransactionResult instance is a container for the results of the transaction.
+ */
+FIR_SWIFT_NAME(TransactionResult)
+@interface FIRTransactionResult : NSObject
+
+/**
+ * Used for runTransactionBlock:. Indicates that the new value should be saved at this location
+ *
+ * @param value A FIRMutableData instance containing the new value to be set
+ * @return An FIRTransactionResult instance that can be used as a return value from the block given to runTransactionBlock:
+ */
++ (FIRTransactionResult *)successWithValue:(FIRMutableData *)value;
+
+
+/**
+ * Used for runTransactionBlock:. Indicates that the current transaction should no longer proceed.
+ *
+ * @return An FIRTransactionResult instance that can be used as a return value from the block given to runTransactionBlock:
+ */
++ (FIRTransactionResult *) abort;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Database/Api/FIRTransactionResult.m b/Firebase/Database/Api/FIRTransactionResult.m
new file mode 100644
index 0000000..8afc5b7
--- /dev/null
+++ b/Firebase/Database/Api/FIRTransactionResult.m
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRTransactionResult.h"
+#import "FIRTransactionResult_Private.h"
+
+@implementation FIRTransactionResult
+
+@synthesize update;
+@synthesize isSuccess;
+
++ (FIRTransactionResult *)successWithValue:(FIRMutableData *)value {
+ FIRTransactionResult * result = [[FIRTransactionResult alloc] init];
+ result.isSuccess = YES;
+ result.update = value;
+ return result;
+}
+
++ (FIRTransactionResult *) abort {
+ FIRTransactionResult * result = [[FIRTransactionResult alloc] init];
+ result.isSuccess = NO;
+ result.update = nil;
+ return result;
+}
+
+@end
diff --git a/Firebase/Database/Api/FirebaseDatabase.h b/Firebase/Database/Api/FirebaseDatabase.h
new file mode 100644
index 0000000..e52f5d6
--- /dev/null
+++ b/Firebase/Database/Api/FirebaseDatabase.h
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef FirebaseDatabase_h
+#define FirebaseDatabase_h
+
+#import "FIRDatabase.h"
+#import "FIRDatabaseQuery.h"
+#import "FIRDatabaseReference.h"
+#import "FIRDataEventType.h"
+#import "FIRDataSnapshot.h"
+#import "FIRMutableData.h"
+#import "FIRServerValue.h"
+#import "FIRTransactionResult.h"
+
+#endif /* FirebaseDatabase_h */
diff --git a/Firebase/Database/Api/Private/FIRDataSnapshot_Private.h b/Firebase/Database/Api/Private/FIRDataSnapshot_Private.h
new file mode 100644
index 0000000..4ff285b
--- /dev/null
+++ b/Firebase/Database/Api/Private/FIRDataSnapshot_Private.h
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIndexedNode.h"
+#import "FTypedefs_Private.h"
+
+@interface FIRDataSnapshot ()
+
+// in _Private for testing purposes
+@property (nonatomic, strong) FIndexedNode *node;
+
+- (id)initWithRef:(FIRDatabaseReference *)ref indexedNode:(FIndexedNode *)node;
+
+@end
diff --git a/Firebase/Database/Api/Private/FIRDatabaseQuery_Private.h b/Firebase/Database/Api/Private/FIRDatabaseQuery_Private.h
new file mode 100644
index 0000000..3a10fe3
--- /dev/null
+++ b/Firebase/Database/Api/Private/FIRDatabaseQuery_Private.h
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FRepo.h"
+#import "FPath.h"
+#import "FRepoManager.h"
+#import "FTypedefs_Private.h"
+#import "FQueryParams.h"
+#import "FIRDatabaseQuery.h"
+
+@interface FIRDatabaseQuery ()
+
++ (dispatch_queue_t)sharedQueue;
+
+- (id) initWithRepo:(FRepo *)repo path:(FPath *)path;
+- (id) initWithRepo:(FRepo *)repo
+ path:(FPath *)path
+ params:(FQueryParams *)params
+ orderByCalled:(BOOL)orderByCalled
+priorityMethodCalled:(BOOL)priorityMethodCalled;
+
+@property (nonatomic, strong) FRepo* repo;
+@property (nonatomic, strong) FPath* path;
+@property (nonatomic, strong) FQueryParams *queryParams;
+@property (nonatomic) BOOL orderByCalled;
+@property (nonatomic) BOOL priorityMethodCalled;
+
+- (FQuerySpec *)querySpec;
+
+@end
diff --git a/Firebase/Database/Api/Private/FIRDatabaseReference_Private.h b/Firebase/Database/Api/Private/FIRDatabaseReference_Private.h
new file mode 100644
index 0000000..cb28feb
--- /dev/null
+++ b/Firebase/Database/Api/Private/FIRDatabaseReference_Private.h
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRDatabaseReference.h"
+#import "FTypedefs_Private.h"
+#import "FIRDatabaseConfig.h"
+#import "FRepo.h"
+
+@interface FIRDatabaseReference ()
+
+- (id)initWithConfig:(FIRDatabaseConfig *)config;
+- (id)initWithRepo:(FRepo *)repo path:(FPath *)path;
+
+// TODO: Update tests to not use this.
++ (FIRDatabaseConfig *)defaultConfig;
+@end
diff --git a/Firebase/Database/Api/Private/FIRDatabase_Private.h b/Firebase/Database/Api/Private/FIRDatabase_Private.h
new file mode 100644
index 0000000..5b7f8cc
--- /dev/null
+++ b/Firebase/Database/Api/Private/FIRDatabase_Private.h
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRDatabase.h"
+
+@class FRepo;
+@class FRepoInfo;
+@class FIRDatabaseConfig;
+
+@interface FIRDatabase ()
+
++ (NSString *) buildVersion;
++ (FIRDatabase *) createDatabaseForTests:(FRepoInfo *)repoInfo config:(FIRDatabaseConfig *)config;
+
+@end
diff --git a/Firebase/Database/Api/Private/FIRMutableData_Private.h b/Firebase/Database/Api/Private/FIRMutableData_Private.h
new file mode 100644
index 0000000..ee3aa96
--- /dev/null
+++ b/Firebase/Database/Api/Private/FIRMutableData_Private.h
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRMutableData.h"
+#import "FNode.h"
+
+@interface FIRMutableData ()
+
+- (id) initWithNode:(id<FNode>)node;
+- (id<FNode>) nodeValue;
+- (BOOL) isEqualToData:(FIRMutableData *)other;
+
+@end
diff --git a/Firebase/Database/Api/Private/FIRTransactionResult_Private.h b/Firebase/Database/Api/Private/FIRTransactionResult_Private.h
new file mode 100644
index 0000000..82290f2
--- /dev/null
+++ b/Firebase/Database/Api/Private/FIRTransactionResult_Private.h
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRTransactionResult.h"
+#import "FIRMutableData.h"
+
+@interface FIRTransactionResult ()
+
+@property (nonatomic) BOOL isSuccess;
+@property (nonatomic, strong) FIRMutableData * update;
+
+@end
diff --git a/Firebase/Database/Api/Private/FTypedefs_Private.h b/Firebase/Database/Api/Private/FTypedefs_Private.h
new file mode 100644
index 0000000..73f4c9a
--- /dev/null
+++ b/Firebase/Database/Api/Private/FTypedefs_Private.h
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef __FTYPEDEFS_PRIVATE__
+#define __FTYPEDEFS_PRIVATE__
+
+#import <Foundation/Foundation.h>
+
+typedef NS_ENUM(NSInteger, FTransactionStatus) {
+ FTransactionInitializing, // 0
+ FTransactionRun, // 1
+ FTransactionSent, // 2
+ FTransactionCompleted, // 3
+ FTransactionSentNeedsAbort, // 4
+ FTransactionNeedsAbort // 5
+};
+
+@protocol FNode;
+@class FPath;
+@class FIRTransactionResult;
+@class FIRMutableData;
+@class FIRDataSnapshot;
+@class FCompoundHash;
+
+typedef void (^fbt_void_nserror_bool_datasnapshot) (NSError* error, BOOL committed, FIRDataSnapshot * snapshot);
+typedef FIRTransactionResult * (^fbt_transactionresult_mutabledata) (FIRMutableData * currentData);
+typedef void (^fbt_void_path_node) (FPath*, id<FNode>);
+typedef void (^fbt_void_nsstring) (NSString *);
+typedef BOOL (^fbt_bool_nsstring_node) (NSString *, id<FNode>);
+typedef void (^fbt_void_path_node_marray) (FPath *, id<FNode>, NSMutableArray *);
+typedef BOOL (^fbt_bool_void) (void);
+typedef void (^fbt_void_nsstring_nsstring)(NSString *str1, NSString* str2);
+typedef void (^fbt_void_nsstring_nserror)(NSString *str, NSError* error);
+typedef BOOL (^fbt_bool_path)(FPath *str);
+typedef void (^fbt_void_id)(id data);
+typedef NSString* (^fbt_nsstring_void) (void);
+typedef FCompoundHash* (^fbt_compoundhash_void) (void);
+typedef NSArray* (^fbt_nsarray_nsstring_id)(NSString *status, id Data);
+typedef NSArray* (^fbt_nsarray_nsstring)(NSString *status);
+
+// WWDC 2012 session 712 starting in page 83 for saving blocks in properties (use @property (strong) type name).
+
+#endif
diff --git a/Firebase/Database/Constants/FConstants.h b/Firebase/Database/Constants/FConstants.h
new file mode 100644
index 0000000..e97a8a1
--- /dev/null
+++ b/Firebase/Database/Constants/FConstants.h
@@ -0,0 +1,190 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef Firebase_FConstants_h
+#define Firebase_FConstants_h
+
+#import <Foundation/Foundation.h>
+
+#pragma mark -
+#pragma mark Wire Protocol Envelope Constants
+
+FOUNDATION_EXPORT NSString *const kFWPRequestType;
+FOUNDATION_EXPORT NSString *const kFWPRequestTypeData;
+FOUNDATION_EXPORT NSString *const kFWPRequestDataPayload;
+FOUNDATION_EXPORT NSString *const kFWPRequestNumber;
+FOUNDATION_EXPORT NSString *const kFWPRequestPayloadBody;
+FOUNDATION_EXPORT NSString *const kFWPRequestError;
+FOUNDATION_EXPORT NSString *const kFWPRequestAction;
+FOUNDATION_EXPORT NSString *const kFWPResponseForRNData;
+FOUNDATION_EXPORT NSString *const kFWPResponseForActionStatus;
+FOUNDATION_EXPORT NSString *const kFWPResponseForActionStatusOk;
+FOUNDATION_EXPORT NSString *const kFWPResponseForActionStatusDataStale;
+FOUNDATION_EXPORT NSString *const kFWPResponseForActionData;
+FOUNDATION_EXPORT NSString *const kFWPResponseDataWarnings;
+
+FOUNDATION_EXPORT NSString *const kFWPAsyncServerAction;
+FOUNDATION_EXPORT NSString *const kFWPAsyncServerPayloadBody;
+FOUNDATION_EXPORT NSString *const kFWPAsyncServerDataUpdate;
+FOUNDATION_EXPORT NSString *const kFWPAsyncServerDataMerge;
+FOUNDATION_EXPORT NSString *const kFWPAsyncServerDataRangeMerge;
+FOUNDATION_EXPORT NSString *const kFWPAsyncServerAuthRevoked;
+FOUNDATION_EXPORT NSString *const kFWPASyncServerListenCancelled;
+FOUNDATION_EXPORT NSString *const kFWPAsyncServerSecurityDebug;
+FOUNDATION_EXPORT NSString *const kFWPAsyncServerDataUpdateBodyPath; // {“a”: “d”, “b”: {“p”: “/”, “d”: “<data>”}}
+FOUNDATION_EXPORT NSString *const kFWPAsyncServerDataUpdateBodyData;
+FOUNDATION_EXPORT NSString *const kFWPAsyncServerDataUpdateStartPath;
+FOUNDATION_EXPORT NSString *const kFWPAsyncServerDataUpdateEndPath;
+FOUNDATION_EXPORT NSString *const kFWPAsyncServerDataUpdateRangeMerge;
+FOUNDATION_EXPORT NSString *const kFWPAsyncServerDataUpdateBodyTag;
+FOUNDATION_EXPORT NSString *const kFWPAsyncServerDataQueries;
+
+FOUNDATION_EXPORT NSString *const kFWPAsyncServerEnvelopeType;
+FOUNDATION_EXPORT NSString *const kFWPAsyncServerEnvelopeData;
+FOUNDATION_EXPORT NSString *const kFWPAsyncServerControlMessage;
+FOUNDATION_EXPORT NSString *const kFWPAsyncServerControlMessageType;
+FOUNDATION_EXPORT NSString *const kFWPAsyncServerControlMessageData;
+FOUNDATION_EXPORT NSString *const kFWPAsyncServerDataMessage;
+
+FOUNDATION_EXPORT NSString *const kFWPAsyncServerHello;
+FOUNDATION_EXPORT NSString *const kFWPAsyncServerHelloTimestamp;
+FOUNDATION_EXPORT NSString *const kFWPAsyncServerHelloVersion;
+FOUNDATION_EXPORT NSString *const kFWPAsyncServerHelloConnectedHost;
+FOUNDATION_EXPORT NSString *const kFWPAsyncServerHelloSession;
+
+FOUNDATION_EXPORT NSString *const kFWPAsyncServerControlMessageShutdown;
+FOUNDATION_EXPORT NSString *const kFWPAsyncServerControlMessageReset;
+
+#pragma mark -
+#pragma mark Wire Protocol Payload Constants
+
+FOUNDATION_EXPORT NSString *const kFWPRequestActionPut;
+FOUNDATION_EXPORT NSString *const kFWPRequestActionMerge;
+FOUNDATION_EXPORT NSString *const kFWPRequestActionTaggedListen;
+FOUNDATION_EXPORT NSString *const kFWPRequestActionTaggedUnlisten;
+FOUNDATION_EXPORT NSString *const kFWPRequestActionListen; // {"t": "d", "d": {"r": 1, "a": "l", "b": { "p": "/" } } }
+FOUNDATION_EXPORT NSString *const kFWPRequestActionUnlisten;
+FOUNDATION_EXPORT NSString *const kFWPRequestActionStats;
+FOUNDATION_EXPORT NSString *const kFWPRequestActionDisconnectPut;
+FOUNDATION_EXPORT NSString *const kFWPRequestActionDisconnectMerge;
+FOUNDATION_EXPORT NSString *const kFWPRequestActionDisconnectCancel;
+FOUNDATION_EXPORT NSString *const kFWPRequestActionAuth;
+FOUNDATION_EXPORT NSString *const kFWPRequestActionUnauth;
+FOUNDATION_EXPORT NSString *const kFWPRequestCredential;
+FOUNDATION_EXPORT NSString *const kFWPRequestPath;
+FOUNDATION_EXPORT NSString *const kFWPRequestCounters;
+FOUNDATION_EXPORT NSString *const kFWPRequestQueries;
+FOUNDATION_EXPORT NSString *const kFWPRequestTag;
+FOUNDATION_EXPORT NSString *const kFWPRequestData;
+FOUNDATION_EXPORT NSString *const kFWPRequestHash;
+FOUNDATION_EXPORT NSString *const kFWPRequestCompoundHash;
+FOUNDATION_EXPORT NSString *const kFWPRequestCompoundHashPaths;
+FOUNDATION_EXPORT NSString *const kFWPRequestCompoundHashHashes;
+FOUNDATION_EXPORT NSString *const kFWPRequestStatus;
+
+#pragma mark -
+#pragma mark Websock Transport Constants
+
+FOUNDATION_EXPORT NSString *const kWireProtocolVersionParam;
+FOUNDATION_EXPORT NSString *const kWebsocketProtocolVersion;
+FOUNDATION_EXPORT NSString *const kWebsocketServerKillPacket;
+FOUNDATION_EXPORT const int kWebsocketMaxFrameSize;
+FOUNDATION_EXPORT NSUInteger const kWebsocketKeepaliveInterval;
+FOUNDATION_EXPORT NSUInteger const kWebsocketConnectTimeout;
+
+FOUNDATION_EXPORT float const kPersistentConnReconnectMinDelay;
+FOUNDATION_EXPORT float const kPersistentConnReconnectMaxDelay;
+FOUNDATION_EXPORT float const kPersistentConnReconnectMultiplier;
+FOUNDATION_EXPORT float const kPersistentConnSuccessfulConnectionEstablishedDelay;
+
+#pragma mark -
+#pragma mark Query / QueryParams constants
+
+FOUNDATION_EXPORT NSString *const kQueryDefault;
+FOUNDATION_EXPORT NSString *const kQueryDefaultObject;
+FOUNDATION_EXPORT NSString *const kViewManagerDictConstView;
+FOUNDATION_EXPORT NSString *const kFQPIndexStartValue;
+FOUNDATION_EXPORT NSString *const kFQPIndexStartName;
+FOUNDATION_EXPORT NSString *const kFQPIndexEndValue;
+FOUNDATION_EXPORT NSString *const kFQPIndexEndName;
+FOUNDATION_EXPORT NSString *const kFQPLimit;
+FOUNDATION_EXPORT NSString *const kFQPViewFrom;
+FOUNDATION_EXPORT NSString *const kFQPViewFromLeft;
+FOUNDATION_EXPORT NSString *const kFQPViewFromRight;
+FOUNDATION_EXPORT NSString *const kFQPIndex;
+
+#pragma mark -
+#pragma mark Interrupt Reasons
+
+FOUNDATION_EXPORT NSString *const kFInterruptReasonServerKill;
+FOUNDATION_EXPORT NSString *const kFInterruptReasonWaitingForOpen;
+FOUNDATION_EXPORT NSString *const kFInterruptReasonRepoInterrupt;
+FOUNDATION_EXPORT NSString *const kFInterruptReasonAuthExpired;
+
+#pragma mark -
+#pragma mark Payload constants
+
+FOUNDATION_EXPORT NSString *const kPayloadPriority;
+FOUNDATION_EXPORT NSString *const kPayloadValue;
+FOUNDATION_EXPORT NSString *const kPayloadMetadataPrefix;
+
+#pragma mark -
+#pragma mark ServerValue constants
+
+FOUNDATION_EXPORT NSString *const kServerValueSubKey;
+FOUNDATION_EXPORT NSString *const kServerValuePriority;
+
+#pragma mark -
+#pragma mark .info/ constants
+
+FOUNDATION_EXPORT NSString *const kDotInfoPrefix;
+FOUNDATION_EXPORT NSString *const kDotInfoConnected;
+FOUNDATION_EXPORT NSString *const kDotInfoServerTimeOffset;
+
+#pragma mark -
+#pragma mark ObjectiveC to JavaScript type constants
+
+FOUNDATION_EXPORT NSString *const kJavaScriptObject;
+FOUNDATION_EXPORT NSString *const kJavaScriptString;
+FOUNDATION_EXPORT NSString *const kJavaScriptBoolean;
+FOUNDATION_EXPORT NSString *const kJavaScriptNumber;
+FOUNDATION_EXPORT NSString *const kJavaScriptNull;
+FOUNDATION_EXPORT NSString *const kJavaScriptTrue;
+FOUNDATION_EXPORT NSString *const kJavaScriptFalse;
+
+#pragma mark -
+#pragma mark Error handling constants
+
+FOUNDATION_EXPORT NSString *const kFErrorDomain;
+FOUNDATION_EXPORT NSUInteger const kFAuthError;
+FOUNDATION_EXPORT NSString *const kFErrorWriteCanceled;
+
+#pragma mark -
+#pragma mark Validation Constants
+
+FOUNDATION_EXPORT NSUInteger const kFirebaseMaxObjectDepth;
+FOUNDATION_EXPORT const unsigned int kFirebaseMaxLeafSize;
+
+#pragma mark -
+#pragma mark Transaction Constants
+
+FOUNDATION_EXPORT NSUInteger const kFTransactionMaxRetries;
+FOUNDATION_EXPORT NSString *const kFTransactionTooManyRetries;
+FOUNDATION_EXPORT NSString *const kFTransactionNoData;
+FOUNDATION_EXPORT NSString *const kFTransactionSet;
+FOUNDATION_EXPORT NSString *const kFTransactionDisconnect;
+
+#endif
diff --git a/Firebase/Database/Constants/FConstants.m b/Firebase/Database/Constants/FConstants.m
new file mode 100644
index 0000000..e492ba1
--- /dev/null
+++ b/Firebase/Database/Constants/FConstants.m
@@ -0,0 +1,183 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FConstants.h"
+
+#pragma mark -
+#pragma mark Wire Protocol Envelope Constants
+
+NSString *const kFWPRequestType = @"t";
+NSString *const kFWPRequestTypeData = @"d";
+NSString *const kFWPRequestDataPayload = @"d";
+NSString *const kFWPRequestNumber = @"r";
+NSString *const kFWPRequestPayloadBody = @"b";
+NSString *const kFWPRequestError = @"error";
+NSString *const kFWPRequestAction = @"a";
+NSString *const kFWPResponseForRNData = @"b";
+NSString *const kFWPResponseForActionStatus = @"s";
+NSString *const kFWPResponseForActionStatusOk = @"ok";
+NSString *const kFWPResponseForActionStatusDataStale = @"datastale";
+NSString *const kFWPResponseForActionData = @"d";
+NSString *const kFWPResponseDataWarnings = @"w";
+NSString *const kFWPAsyncServerAction = @"a";
+NSString *const kFWPAsyncServerPayloadBody = @"b";
+NSString *const kFWPAsyncServerDataUpdate = @"d";
+NSString *const kFWPAsyncServerDataMerge = @"m";
+NSString *const kFWPAsyncServerDataRangeMerge = @"rm";
+NSString *const kFWPAsyncServerAuthRevoked = @"ac";
+NSString *const kFWPASyncServerListenCancelled = @"c";
+NSString *const kFWPAsyncServerSecurityDebug = @"sd";
+NSString *const kFWPAsyncServerDataUpdateBodyPath = @"p"; // {“a”: “d”, “b”: {“p”: “/”, “d”: “<data>”}}
+NSString *const kFWPAsyncServerDataUpdateBodyData = @"d";
+NSString *const kFWPAsyncServerDataUpdateStartPath = @"s";
+NSString *const kFWPAsyncServerDataUpdateEndPath = @"e";
+NSString *const kFWPAsyncServerDataUpdateRangeMerge = @"m";
+NSString *const kFWPAsyncServerDataUpdateBodyTag = @"t";
+NSString *const kFWPAsyncServerDataQueries = @"q";
+
+NSString *const kFWPAsyncServerEnvelopeType = @"t";
+NSString *const kFWPAsyncServerEnvelopeData = @"d";
+NSString *const kFWPAsyncServerControlMessage = @"c";
+NSString *const kFWPAsyncServerControlMessageType = @"t";
+NSString *const kFWPAsyncServerControlMessageData = @"d";
+NSString *const kFWPAsyncServerDataMessage = @"d";
+
+NSString *const kFWPAsyncServerHello = @"h";
+NSString *const kFWPAsyncServerHelloTimestamp = @"ts";
+NSString *const kFWPAsyncServerHelloVersion = @"v";
+NSString *const kFWPAsyncServerHelloConnectedHost = @"h";
+NSString *const kFWPAsyncServerHelloSession = @"s";
+
+NSString *const kFWPAsyncServerControlMessageShutdown = @"s";
+NSString *const kFWPAsyncServerControlMessageReset = @"r";
+
+#pragma mark -
+#pragma mark Wire Protocol Payload Constants
+
+NSString *const kFWPRequestActionPut = @"p";
+NSString *const kFWPRequestActionMerge = @"m";
+NSString *const kFWPRequestActionListen = @"l"; // {"t": "d", "d": {"r": 1, "a": "l", "b": { "p": "/" } } }
+NSString *const kFWPRequestActionUnlisten = @"u";
+NSString *const kFWPRequestActionStats = @"s";
+NSString *const kFWPRequestActionTaggedListen = @"q";
+NSString *const kFWPRequestActionTaggedUnlisten = @"n";
+NSString *const kFWPRequestActionDisconnectPut = @"o";
+NSString *const kFWPRequestActionDisconnectMerge = @"om";
+NSString *const kFWPRequestActionDisconnectCancel = @"oc";
+NSString *const kFWPRequestActionAuth = @"auth";
+NSString *const kFWPRequestActionUnauth = @"unauth";
+NSString *const kFWPRequestCredential = @"cred";
+NSString *const kFWPRequestPath = @"p";
+NSString *const kFWPRequestCounters = @"c";
+NSString *const kFWPRequestQueries = @"q";
+NSString *const kFWPRequestTag = @"t";
+NSString *const kFWPRequestData = @"d";
+NSString *const kFWPRequestHash = @"h";
+NSString *const kFWPRequestCompoundHash = @"ch";
+NSString *const kFWPRequestCompoundHashPaths = @"ps";
+NSString *const kFWPRequestCompoundHashHashes = @"hs";
+NSString *const kFWPRequestStatus = @"s";
+
+#pragma mark -
+#pragma mark Websock Transport Constants
+
+NSString *const kWireProtocolVersionParam = @"v";
+NSString *const kWebsocketProtocolVersion = @"5";
+NSString *const kWebsocketServerKillPacket = @"kill";
+const int kWebsocketMaxFrameSize = 16384;
+NSUInteger const kWebsocketKeepaliveInterval = 45;
+NSUInteger const kWebsocketConnectTimeout = 30;
+
+float const kPersistentConnReconnectMinDelay = 1.0;
+float const kPersistentConnReconnectMaxDelay = 30.0;
+float const kPersistentConnReconnectMultiplier = 1.3f;
+float const kPersistentConnSuccessfulConnectionEstablishedDelay = 30.0;
+
+#pragma mark -
+#pragma mark Query constants
+
+NSString *const kQueryDefault = @"default";
+NSString *const kQueryDefaultObject = @"{}";
+NSString *const kViewManagerDictConstView = @"view";
+NSString *const kFQPIndexStartValue = @"sp";
+NSString *const kFQPIndexStartName = @"sn";
+NSString *const kFQPIndexEndValue = @"ep";
+NSString *const kFQPIndexEndName = @"en";
+NSString *const kFQPLimit = @"l";
+NSString *const kFQPViewFrom = @"vf";
+NSString *const kFQPViewFromLeft = @"l";
+NSString *const kFQPViewFromRight = @"r";
+NSString *const kFQPIndex = @"i";
+
+#pragma mark -
+#pragma mark Interrupt Reasons
+
+NSString *const kFInterruptReasonServerKill = @"server_kill";
+NSString *const kFInterruptReasonWaitingForOpen = @"waiting_for_open";
+NSString *const kFInterruptReasonRepoInterrupt = @"repo_interrupt";
+
+#pragma mark -
+#pragma mark Payload constants
+
+NSString *const kPayloadPriority = @".priority";
+NSString *const kPayloadValue = @".value";
+NSString *const kPayloadMetadataPrefix = @".";
+
+#pragma mark -
+#pragma mark ServerValue constants
+
+NSString *const kServerValueSubKey = @".sv";
+NSString *const kServerValuePriority = @"timestamp";
+
+#pragma mark -
+#pragma mark .info/ constants
+
+NSString *const kDotInfoPrefix = @".info";
+NSString *const kDotInfoConnected = @"connected";
+NSString *const kDotInfoServerTimeOffset = @"serverTimeOffset";
+
+#pragma mark -
+#pragma mark ObjectiveC to JavaScript type constants
+
+NSString *const kJavaScriptObject = @"object";
+NSString *const kJavaScriptString = @"string";
+NSString *const kJavaScriptBoolean = @"boolean";
+NSString *const kJavaScriptNumber = @"number";
+NSString *const kJavaScriptNull = @"null";
+NSString *const kJavaScriptTrue = @"true";
+NSString *const kJavaScriptFalse = @"false";
+
+#pragma mark -
+#pragma mark Error handling constants
+
+NSString *const kFErrorDomain = @"com.firebase";
+NSUInteger const kFAuthError = 1;
+NSString *const kFErrorWriteCanceled = @"write_canceled";
+
+#pragma mark -
+#pragma mark Validation Constants
+
+NSUInteger const kFirebaseMaxObjectDepth = 1000;
+const unsigned int kFirebaseMaxLeafSize = 1024 * 1024 * 10; // 10 MB
+
+#pragma mark -
+#pragma mark Transaction Constants
+
+NSUInteger const kFTransactionMaxRetries = 25;
+NSString *const kFTransactionTooManyRetries = @"maxretry";
+NSString *const kFTransactionNoData = @"nodata";
+NSString *const kFTransactionSet = @"set";
+NSString *const kFTransactionDisconnect = @"disconnect";
diff --git a/Firebase/Database/Core/FCompoundHash.h b/Firebase/Database/Core/FCompoundHash.h
new file mode 100644
index 0000000..cd5240e
--- /dev/null
+++ b/Firebase/Database/Core/FCompoundHash.h
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+#import "FNode.h"
+
+
+@interface FCompoundHashBuilder : NSObject
+
+- (FPath *)currentPath;
+
+@end
+
+
+typedef BOOL (^FCompoundHashSplitStrategy) (FCompoundHashBuilder *builder);
+
+
+@interface FCompoundHash : NSObject
+
+@property (nonatomic, strong, readonly) NSArray *posts;
+@property (nonatomic, strong, readonly) NSArray *hashes;
+
++ (FCompoundHash *)fromNode:(id<FNode>)node;
++ (FCompoundHash *)fromNode:(id<FNode>)node splitStrategy:(FCompoundHashSplitStrategy)strategy;
+
+@end
diff --git a/Firebase/Database/Core/FCompoundHash.m b/Firebase/Database/Core/FCompoundHash.m
new file mode 100644
index 0000000..b4f72cd
--- /dev/null
+++ b/Firebase/Database/Core/FCompoundHash.m
@@ -0,0 +1,236 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FCompoundHash.h"
+#import "FLeafNode.h"
+#import "FStringUtilities.h"
+#import "FSnapshotUtilities.h"
+#import "FChildrenNode.h"
+
+@interface FCompoundHashBuilder ()
+
+@property (nonatomic, strong) FCompoundHashSplitStrategy splitStrategy;
+
+@property (nonatomic, strong) NSMutableArray *currentPaths;
+@property (nonatomic, strong) NSMutableArray *currentHashes;
+
+@end
+
+@implementation FCompoundHashBuilder {
+
+ // NOTE: We use the existence of this to know if we've started building a range (i.e. encountered a leaf node).
+ NSMutableString *optHashValueBuilder;
+
+ // The current path as a stack. This is used in combination with currentPathDepth to simultaneously store the
+ // last leaf node path. The depth is changed when descending and ascending, at the same time the current key
+ // is set for the current depth. Because the keys are left unchanged for ascending the path will also contain
+ // the path of the last visited leaf node (using lastLeafDepth elements)
+ NSMutableArray *currentPath;
+ NSInteger lastLeafDepth;
+ NSInteger currentPathDepth;
+
+ BOOL needsComma;
+}
+
+- (instancetype)initWithSplitStrategy:(FCompoundHashSplitStrategy)strategy {
+ self = [super init];
+ if (self != nil) {
+ self->_splitStrategy = strategy;
+ self->optHashValueBuilder = nil;
+ self->currentPath = [NSMutableArray array];
+ self->lastLeafDepth = -1;
+ self->currentPathDepth = 0;
+ self->needsComma = YES;
+ self->_currentPaths = [NSMutableArray array];
+ self->_currentHashes = [NSMutableArray array];
+ }
+ return self;
+}
+
+- (BOOL)isBuildingRange {
+ return self->optHashValueBuilder != nil;
+}
+
+- (NSUInteger)currentHashLength {
+ return self->optHashValueBuilder.length;
+}
+
+- (FPath *)currentPath {
+ return [self currentPathWithDepth:self->currentPathDepth];
+}
+
+- (FPath *)currentPathWithDepth:(NSInteger)depth {
+ NSArray *pieces = [self->currentPath subarrayWithRange:NSMakeRange(0, depth)];
+ return [[FPath alloc] initWithPieces:pieces andPieceNum:0];
+}
+
+- (void)enumerateCurrentPathToDepth:(NSInteger)depth withBlock:(void (^) (NSString *key))block {
+ for (NSInteger i = 0; i < depth; i++) {
+ block(self->currentPath[i]);
+ }
+}
+
+- (void)appendKey:(NSString *)key toString:(NSMutableString *)string {
+ [FSnapshotUtilities appendHashV2RepresentationForString:key toString:string];
+}
+
+- (void)ensureRange {
+ if (![self isBuildingRange]) {
+ optHashValueBuilder = [NSMutableString string];
+ [optHashValueBuilder appendString:@"("];
+ [self enumerateCurrentPathToDepth:self->currentPathDepth withBlock:^(NSString *key) {
+ [self appendKey:key toString:self->optHashValueBuilder];
+ [self->optHashValueBuilder appendString:@":("];
+ }];
+ self->needsComma = NO;
+ }
+}
+
+- (void)processLeaf:(FLeafNode *)leafNode {
+ [self ensureRange];
+
+ self->lastLeafDepth = self->currentPathDepth;
+ [FSnapshotUtilities appendHashRepresentationForLeafNode:leafNode
+ toString:self->optHashValueBuilder
+ hashVersion:FDataHashVersionV2];
+ self->needsComma = YES;
+ if (self.splitStrategy(self)) {
+ [self endRange];
+ }
+}
+
+- (void)startChild:(NSString *)key {
+ [self ensureRange];
+
+ if (self->needsComma) {
+ [self->optHashValueBuilder appendString:@","];
+ }
+ [self appendKey:key toString:self->optHashValueBuilder];
+ [self->optHashValueBuilder appendString:@":("];
+ if (self->currentPathDepth == currentPath.count) {
+ [self->currentPath addObject:key];
+ } else {
+ self->currentPath[self->currentPathDepth] = key;
+ }
+ self->currentPathDepth++;
+ self->needsComma = NO;
+}
+
+- (void)endChild {
+ self->currentPathDepth--;
+ if ([self isBuildingRange]) {
+ [self->optHashValueBuilder appendString:@")"];
+ }
+ self->needsComma = YES;
+}
+
+- (void)finishHashing {
+ NSAssert(self->currentPathDepth == 0, @"Can't finish hashing in the middle of processing a child");
+ if ([self isBuildingRange] ) {
+ [self endRange];
+ }
+
+ // Always close with the empty hash for the remaining range to allow simple appending
+ [self.currentHashes addObject:@""];
+}
+
+- (void)endRange {
+ NSAssert([self isBuildingRange], @"Can't end range without starting a range!");
+ // Add closing parenthesis for current depth
+ for (NSUInteger i = 0; i < currentPathDepth; i++) {
+ [self->optHashValueBuilder appendString:@")"];
+ }
+ [self->optHashValueBuilder appendString:@")"];
+
+ FPath *lastLeafPath = [self currentPathWithDepth:self->lastLeafDepth];
+ NSString *hash = [FStringUtilities base64EncodedSha1:self->optHashValueBuilder];
+ [self.currentHashes addObject:hash];
+ [self.currentPaths addObject:lastLeafPath];
+
+ self->optHashValueBuilder = nil;
+}
+
+@end
+
+
+@interface FCompoundHash ()
+
+@property (nonatomic, strong, readwrite) NSArray *posts;
+@property (nonatomic, strong, readwrite) NSArray *hashes;
+
+@end
+
+@implementation FCompoundHash
+
+- (id)initWithPosts:(NSArray *)posts hashes:(NSArray *)hashes {
+ self = [super init];
+ if (self != nil) {
+ if (posts.count != hashes.count - 1) {
+ [NSException raise:NSInvalidArgumentException format:@"Number of posts need to be n-1 for n hashes in FCompoundHash"];
+ }
+ self.posts = posts;
+ self.hashes = hashes;
+ }
+ return self;
+}
+
++ (FCompoundHashSplitStrategy)simpleSizeSplitStrategyForNode:(id<FNode>)node {
+ NSUInteger estimatedSize = [FSnapshotUtilities estimateSerializedNodeSize:node];
+
+ // Splits for
+ // 1k -> 512 (2 parts)
+ // 5k -> 715 (7 parts)
+ // 100k -> 3.2k (32 parts)
+ // 500k -> 7k (71 parts)
+ // 5M -> 23k (228 parts)
+ NSUInteger splitThreshold = MAX(512, (NSUInteger)sqrt(estimatedSize * 100));
+
+ return ^BOOL(FCompoundHashBuilder *builder) {
+ // Never split on priorities
+ return [builder currentHashLength] > splitThreshold && ![[[builder currentPath] getBack] isEqualToString:@".priority"];
+ };
+}
+
++ (FCompoundHash *)fromNode:(id<FNode>)node {
+ return [FCompoundHash fromNode:node splitStrategy:[FCompoundHash simpleSizeSplitStrategyForNode:node]];
+}
+
++ (FCompoundHash *)fromNode:(id<FNode>)node splitStrategy:(FCompoundHashSplitStrategy)strategy {
+ if ([node isEmpty]) {
+ return [[FCompoundHash alloc] initWithPosts:@[] hashes:@[@""]];
+ } else {
+ FCompoundHashBuilder *builder = [[FCompoundHashBuilder alloc] initWithSplitStrategy:strategy];
+ [FCompoundHash processNode:node builder:builder];
+ [builder finishHashing];
+ return [[FCompoundHash alloc] initWithPosts:builder.currentPaths hashes:builder.currentHashes];
+ }
+}
+
++ (void)processNode:(id<FNode>)node builder:(FCompoundHashBuilder *)builder {
+ if ([node isLeafNode]) {
+ [builder processLeaf:node];
+ } else {
+ NSAssert(![node isEmpty], @"Can't calculate hash on empty node!");
+ FChildrenNode *childrenNode = (FChildrenNode *)node;
+ [childrenNode enumerateChildrenAndPriorityUsingBlock:^(NSString *key, id<FNode> node, BOOL *stop) {
+ [builder startChild:key];
+ [self processNode:node builder:builder];
+ [builder endChild];
+ }];
+ }
+}
+
+@end
diff --git a/Firebase/Database/Core/FListenProvider.h b/Firebase/Database/Core/FListenProvider.h
new file mode 100644
index 0000000..7a41754
--- /dev/null
+++ b/Firebase/Database/Core/FListenProvider.h
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FTypedefs_Private.h"
+
+@class FQuerySpec;
+@protocol FSyncTreeHash;
+
+typedef NSArray* (^fbt_startListeningBlock)(FQuerySpec *query,
+ NSNumber *tagId,
+ id<FSyncTreeHash> hash,
+ fbt_nsarray_nsstring onComplete);
+typedef void (^fbt_stopListeningBlock)(FQuerySpec *query, NSNumber *tagId);
+
+@interface FListenProvider : NSObject
+
+@property (nonatomic, copy) fbt_startListeningBlock startListening;
+@property (nonatomic, copy) fbt_stopListeningBlock stopListening;
+
+@end
diff --git a/Firebase/Database/Core/FListenProvider.m b/Firebase/Database/Core/FListenProvider.m
new file mode 100644
index 0000000..7a49609
--- /dev/null
+++ b/Firebase/Database/Core/FListenProvider.m
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FListenProvider.h"
+#import "FIRDatabaseQuery.h"
+
+
+@implementation FListenProvider
+
+@synthesize startListening;
+@synthesize stopListening;
+
+@end
diff --git a/Firebase/Database/Core/FPersistentConnection.h b/Firebase/Database/Core/FPersistentConnection.h
new file mode 100644
index 0000000..412c874
--- /dev/null
+++ b/Firebase/Database/Core/FPersistentConnection.h
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+#import "FConnection.h"
+#import "FRepoInfo.h"
+#import "FTypedefs.h"
+#import "FTypedefs_Private.h"
+
+@protocol FPersistentConnectionDelegate;
+@protocol FSyncTreeHash;
+@class FQuerySpec;
+@class FIRDatabaseConfig;
+
+@interface FPersistentConnection : NSObject <FConnectionDelegate>
+
+@property (nonatomic, weak) id <FPersistentConnectionDelegate> delegate;
+@property (nonatomic) BOOL pauseWrites;
+
+- (id)initWithRepoInfo:(FRepoInfo *)repoInfo
+ dispatchQueue:(dispatch_queue_t)queue
+ config:(FIRDatabaseConfig *)config;
+
+- (void)open;
+
+- (void) putData:(id)data forPath:(NSString *)pathString withHash:(NSString *)hash withCallback:(fbt_void_nsstring_nsstring)onComplete;
+- (void) mergeData:(id)data forPath:(NSString *)pathString withCallback:(fbt_void_nsstring_nsstring)onComplete;
+
+- (void) listen:(FQuerySpec *)query
+ tagId:(NSNumber *)tagId
+ hash:(id<FSyncTreeHash>)hash
+ onComplete:(fbt_void_nsstring)onComplete;
+
+- (void) unlisten:(FQuerySpec *)query tagId:(NSNumber *)tagId;
+- (void) refreshAuthToken:(NSString *)token;
+- (void) onDisconnectPutData:(id)data forPath:(FPath *)path withCallback:(fbt_void_nsstring_nsstring)callback;
+- (void) onDisconnectMergeData:(id)data forPath:(FPath *)path withCallback:(fbt_void_nsstring_nsstring)callback;
+- (void) onDisconnectCancelPath:(FPath *)path withCallback:(fbt_void_nsstring_nsstring)callback;
+- (void) ackPuts;
+- (void) purgeOutstandingWrites;
+
+- (void) interruptForReason:(NSString *)reason;
+- (void) resumeForReason:(NSString *)reason;
+- (BOOL) isInterruptedForReason:(NSString *)reason;
+
+// FConnection delegate methods
+- (void)onReady:(FConnection *)fconnection atTime:(NSNumber *)timestamp sessionID:(NSString *)sessionID;
+- (void)onDataMessage:(FConnection *)fconnection withMessage:(NSDictionary *)message;
+- (void)onDisconnect:(FConnection *)fconnection withReason:(FDisconnectReason)reason;
+- (void)onKill:(FConnection *)fconnection withReason:(NSString *)reason;
+
+// Testing methods
+- (NSDictionary *) dumpListens;
+
+@end
+
+@protocol FPersistentConnectionDelegate <NSObject>
+
+- (void)onDataUpdate:(FPersistentConnection *)fpconnection forPath:(NSString *)pathString message:(id)message isMerge:(BOOL)isMerge tagId:(NSNumber *)tagId;
+- (void)onRangeMerge:(NSArray *)ranges forPath:(NSString *)path tagId:(NSNumber *)tag;
+- (void)onConnect:(FPersistentConnection *)fpconnection;
+- (void)onDisconnect:(FPersistentConnection *)fpconnection;
+- (void)onServerInfoUpdate:(FPersistentConnection *)fpconnection updates:(NSDictionary *)updates;
+
+@end
diff --git a/Firebase/Database/Core/FPersistentConnection.m b/Firebase/Database/Core/FPersistentConnection.m
new file mode 100644
index 0000000..0eb1f9f
--- /dev/null
+++ b/Firebase/Database/Core/FPersistentConnection.m
@@ -0,0 +1,945 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <SystemConfiguration/SystemConfiguration.h>
+#import <netinet/in.h>
+#import <dlfcn.h>
+#import "FIRDatabaseReference.h"
+#import "FPersistentConnection.h"
+#import "FConstants.h"
+#import "FAtomicNumber.h"
+#import "FQueryParams.h"
+#import "FTupleOnDisconnect.h"
+#import "FTupleCallbackStatus.h"
+#import "FQuerySpec.h"
+#import "FIndex.h"
+#import "FIRDatabaseConfig.h"
+#import "FIRDatabaseConfig_Private.h"
+#import "FSnapshotUtilities.h"
+#import "FRangeMerge.h"
+#import "FCompoundHash.h"
+#import "FSyncTree.h"
+#import "FIRRetryHelper.h"
+#import "FAuthTokenProvider.h"
+#import "FUtilities.h"
+
+@interface FOutstandingQuery : NSObject
+
+@property (nonatomic, strong) FQuerySpec* query;
+@property (nonatomic, strong) NSNumber *tagId;
+@property (nonatomic, strong) id<FSyncTreeHash> syncTreeHash;
+@property (nonatomic, copy) fbt_void_nsstring onComplete;
+
+@end
+
+@implementation FOutstandingQuery
+
+@end
+
+
+@interface FOutstandingPut : NSObject
+
+@property (nonatomic, strong) NSString *action;
+@property (nonatomic, strong) NSDictionary *request;
+@property (nonatomic, copy) fbt_void_nsstring_nsstring onCompleteBlock;
+@property (nonatomic) BOOL sent;
+
+@end
+
+@implementation FOutstandingPut
+
+@end
+
+
+typedef enum {
+ ConnectionStateDisconnected,
+ ConnectionStateGettingToken,
+ ConnectionStateConnecting,
+ ConnectionStateAuthenticating,
+ ConnectionStateConnected
+} ConnectionState;
+
+@interface FPersistentConnection () {
+ ConnectionState connectionState;
+ BOOL firstConnection;
+ NSTimeInterval reconnectDelay;
+ NSTimeInterval lastConnectionAttemptTime;
+ NSTimeInterval lastConnectionEstablishedTime;
+ SCNetworkReachabilityRef reachability;
+}
+
+- (int) getNextRequestNumber;
+- (void) onDataPushWithAction:(NSString *)action andBody:(NSDictionary *)body;
+- (void) handleTimestamp:(NSNumber *)timestamp;
+- (void) sendOnDisconnectAction:(NSString *)action forPath:(NSString *)pathString withData:(id)data andCallback:(fbt_void_nsstring_nsstring)callback;
+
+@property (nonatomic, strong) FConnection* realtime;
+@property (nonatomic, strong) NSMutableDictionary* listens;
+@property (nonatomic, strong) NSMutableDictionary* outstandingPuts;
+@property (nonatomic, strong) NSMutableArray* onDisconnectQueue;
+@property (nonatomic, strong) FRepoInfo* repoInfo;
+@property (nonatomic, strong) FAtomicNumber* putCounter;
+@property (nonatomic, strong) FAtomicNumber* requestNumber;
+@property (nonatomic, strong) NSMutableDictionary* requestCBHash;
+@property (nonatomic, strong) FIRDatabaseConfig *config;
+@property (nonatomic) NSUInteger unackedListensCount;
+@property (nonatomic, strong) NSMutableArray *putsToAck;
+@property (nonatomic, strong) dispatch_queue_t dispatchQueue;
+@property (nonatomic, strong) NSString* lastSessionID;
+@property (nonatomic, strong) NSMutableSet *interruptReasons;
+@property (nonatomic, strong) FIRRetryHelper *retryHelper;
+@property (nonatomic, strong) id<FAuthTokenProvider> authTokenProvider;
+@property (nonatomic, strong) NSString *authToken;
+@property (nonatomic) BOOL forceAuthTokenRefresh;
+@property (nonatomic) NSUInteger currentFetchTokenAttempt;
+
+@end
+
+
+@implementation FPersistentConnection
+
+- (id)initWithRepoInfo:(FRepoInfo *)repoInfo dispatchQueue:(dispatch_queue_t)dispatchQueue config:(FIRDatabaseConfig *)config {
+ self = [super init];
+ if (self) {
+ self->_config = config;
+ self->_repoInfo = repoInfo;
+ self->_dispatchQueue = dispatchQueue;
+ self->_authTokenProvider = config.authTokenProvider;
+ NSAssert(self->_authTokenProvider != nil, @"Expected auth token provider");
+ self.interruptReasons = [NSMutableSet set];
+
+ self.listens = [[NSMutableDictionary alloc] init];
+ self.outstandingPuts = [[NSMutableDictionary alloc] init];
+ self.onDisconnectQueue = [[NSMutableArray alloc] init];
+ self.putCounter = [[FAtomicNumber alloc] init];
+ self.requestNumber = [[FAtomicNumber alloc] init];
+ self.requestCBHash = [[NSMutableDictionary alloc] init];
+ self.unackedListensCount = 0;
+ self.putsToAck = [NSMutableArray array];
+ connectionState = ConnectionStateDisconnected;
+ firstConnection = YES;
+ reconnectDelay = kPersistentConnReconnectMinDelay;
+
+ self->_retryHelper = [[FIRRetryHelper alloc] initWithDispatchQueue:dispatchQueue
+ minRetryDelayAfterFailure:kPersistentConnReconnectMinDelay
+ maxRetryDelay:kPersistentConnReconnectMaxDelay
+ retryExponent:kPersistentConnReconnectMultiplier
+ jitterFactor:0.7];
+
+ [self setupNotifications];
+ // Make sure we don't actually connect until open is called
+ [self interruptForReason:kFInterruptReasonWaitingForOpen];
+ }
+ // nb: The reason establishConnection isn't called here like the JS version is because
+ // callers need to set the delegate first. The ctor can be modified to accept the delegate
+ // but that deviates from normal ios conventions. After the delegate has been set, the caller
+ // is responsible for calling establishConnection:
+ return self;
+}
+
+- (void) dealloc {
+ if (reachability) {
+ // Unschedule the notifications
+ SCNetworkReachabilitySetDispatchQueue(reachability, NULL);
+ CFRelease(reachability);
+ }
+}
+
+#pragma mark -
+#pragma mark Public methods
+
+- (void) open {
+ [self resumeForReason:kFInterruptReasonWaitingForOpen];
+}
+
+/**
+* Note that the listens dictionary has a type of Map[String (pathString), Map[FQueryParams, FOutstandingQuery]]
+*
+* This means, for each path we care about, there are sets of queryParams that correspond to an FOutstandingQuery object.
+* There can be multiple sets at a path since we overlap listens for a short time while adding or removing a query from a
+* location in the tree.
+*/
+- (void) listen:(FQuerySpec *)query
+ tagId:(NSNumber *)tagId
+ hash:(id<FSyncTreeHash>)hash
+ onComplete:(fbt_void_nsstring)onComplete {
+ FFLog(@"I-RDB034001", @"Listen called for %@", query);
+
+ NSAssert(self.listens[query] == nil, @"listen() called twice for the same query");
+ NSAssert(query.isDefault || !query.loadsAllData, @"listen called for non-default but complete query");
+ FOutstandingQuery* outstanding = [[FOutstandingQuery alloc] init];
+ outstanding.query = query;
+ outstanding.tagId = tagId;
+ outstanding.syncTreeHash = hash;
+ outstanding.onComplete = onComplete;
+ [self.listens setObject:outstanding forKey:query];
+ if ([self connected]) {
+ [self sendListen:outstanding];
+ }
+}
+
+- (void) putData:(id)data forPath:(NSString *)pathString withHash:(NSString *)hash withCallback:(fbt_void_nsstring_nsstring)onComplete {
+ [self putInternal:data forAction:kFWPRequestActionPut forPath:pathString withHash:hash withCallback:onComplete];
+}
+
+- (void) mergeData:(id)data forPath:(NSString *)pathString withCallback:(fbt_void_nsstring_nsstring)onComplete {
+ [self putInternal:data forAction:kFWPRequestActionMerge forPath:pathString withHash:nil withCallback:onComplete];
+}
+
+- (void) onDisconnectPutData:(id)data forPath:(FPath *)path withCallback:(fbt_void_nsstring_nsstring)callback {
+ if ([self canSendWrites]) {
+ [self sendOnDisconnectAction:kFWPRequestActionDisconnectPut forPath:[path description] withData:data andCallback:callback];
+ } else {
+ FTupleOnDisconnect* tuple = [[FTupleOnDisconnect alloc] init];
+ tuple.pathString = [path description];
+ tuple.action = kFWPRequestActionDisconnectPut;
+ tuple.data = data;
+ tuple.onComplete = callback;
+ [self.onDisconnectQueue addObject:tuple];
+ }
+}
+
+- (void) onDisconnectMergeData:(id)data forPath:(FPath *)path withCallback:(fbt_void_nsstring_nsstring)callback {
+ if ([self canSendWrites]) {
+ [self sendOnDisconnectAction:kFWPRequestActionDisconnectMerge forPath:[path description] withData:data andCallback:callback];
+ } else {
+ FTupleOnDisconnect* tuple = [[FTupleOnDisconnect alloc] init];
+ tuple.pathString = [path description];
+ tuple.action = kFWPRequestActionDisconnectMerge;
+ tuple.data = data;
+ tuple.onComplete = callback;
+ [self.onDisconnectQueue addObject:tuple];
+ }
+}
+
+- (void) onDisconnectCancelPath:(FPath *)path withCallback:(fbt_void_nsstring_nsstring)callback {
+ if ([self canSendWrites]) {
+ [self sendOnDisconnectAction:kFWPRequestActionDisconnectCancel forPath:[path description] withData:[NSNull null] andCallback:callback];
+ } else {
+ FTupleOnDisconnect* tuple = [[FTupleOnDisconnect alloc] init];
+ tuple.pathString = [path description];
+ tuple.action = kFWPRequestActionDisconnectCancel;
+ tuple.data = [NSNull null];
+ tuple.onComplete = callback;
+ [self.onDisconnectQueue addObject:tuple];
+ }
+}
+
+- (void) unlisten:(FQuerySpec *)query tagId:(NSNumber *)tagId {
+ FPath *path = query.path;
+ FFLog(@"I-RDB034002", @"Unlistening for %@", query);
+
+ NSArray *outstanding = [self removeListen:query];
+ if (outstanding.count > 0 && [self connected]) {
+ [self sendUnlisten:path queryParams:query.params tagId:tagId];
+ }
+}
+
+- (void) refreshAuthToken:(NSString *)token {
+ self.authToken = token;
+ if ([self connected]) {
+ if (token != nil) {
+ [self sendAuthAndRestoreStateAfterComplete:NO];
+ } else {
+ [self sendUnauth];
+ }
+ }
+}
+
+#pragma mark -
+#pragma mark Connection status
+
+- (BOOL)connected {
+ return self->connectionState == ConnectionStateAuthenticating || self->connectionState == ConnectionStateConnected;
+}
+
+- (BOOL)canSendWrites {
+ return self->connectionState == ConnectionStateConnected;
+}
+
+#pragma mark -
+#pragma mark FConnection delegate methods
+
+- (void)onReady:(FConnection *)fconnection atTime:(NSNumber *)timestamp sessionID:(NSString *)sessionID {
+ FFLog(@"I-RDB034003", @"On ready");
+ lastConnectionEstablishedTime = [[NSDate date] timeIntervalSince1970];
+ [self handleTimestamp:timestamp];
+
+ if (firstConnection) {
+ [self sendConnectStats];
+ }
+
+ [self restoreAuth];
+ firstConnection = NO;
+ self.lastSessionID = sessionID;
+ dispatch_async(self.dispatchQueue, ^{
+ [self.delegate onConnect:self];
+ });
+}
+
+- (void)onDataMessage:(FConnection *)fconnection withMessage:(NSDictionary *)message {
+ if (message[kFWPRequestNumber] != nil) {
+ // this is a response to a request we sent
+ NSNumber* rn = [NSNumber numberWithInt:[[message objectForKey:kFWPRequestNumber] intValue]];
+ if ([self.requestCBHash objectForKey:rn]) {
+ void (^callback)(NSDictionary*) = [self.requestCBHash objectForKey:rn];
+ [self.requestCBHash removeObjectForKey:rn];
+
+ if (callback) {
+ //dispatch_async(self.dispatchQueue, ^{
+ callback([message objectForKey:kFWPResponseForRNData]);
+ //});
+ }
+ }
+ } else if (message[kFWPRequestError] != nil) {
+ NSString* error = [message objectForKey:kFWPRequestError];
+ @throw [[NSException alloc] initWithName:@"FirebaseDatabaseServerError" reason:error userInfo:nil];
+ } else if (message[kFWPAsyncServerAction] != nil) {
+ // this is a server push of some sort
+ NSString* action = [message objectForKey:kFWPAsyncServerAction];
+ NSDictionary* body = [message objectForKey:kFWPAsyncServerPayloadBody];
+ [self onDataPushWithAction:action andBody:body];
+ }
+}
+
+- (void)onDisconnect:(FConnection *)fconnection withReason:(FDisconnectReason)reason {
+ FFLog(@"I-RDB034004", @"Got on disconnect due to %s", (reason == DISCONNECT_REASON_SERVER_RESET) ? "server_reset" : "other");
+ connectionState = ConnectionStateDisconnected;
+ // Drop the realtime connection
+ self.realtime = nil;
+ [self cancelSentTransactions];
+ [self.requestCBHash removeAllObjects];
+ self.unackedListensCount = 0;
+ if ([self shouldReconnect]) {
+ NSTimeInterval timeSinceLastConnectSucceeded = [[NSDate date] timeIntervalSince1970] - lastConnectionEstablishedTime;
+ BOOL lastConnectionWasSuccessful;
+ if (lastConnectionEstablishedTime > 0) {
+ lastConnectionWasSuccessful = timeSinceLastConnectSucceeded > kPersistentConnSuccessfulConnectionEstablishedDelay;
+ } else {
+ lastConnectionWasSuccessful = NO;
+ }
+
+ if (reason == DISCONNECT_REASON_SERVER_RESET || lastConnectionWasSuccessful) {
+ [self.retryHelper signalSuccess];
+ }
+ [self tryScheduleReconnect];
+ }
+ lastConnectionEstablishedTime = 0;
+ [self.delegate onDisconnect:self];
+}
+
+- (void)onKill:(FConnection *)fconnection withReason:(NSString *)reason {
+ FFWarn(@"I-RDB034005", @"Firebase Database connection was forcefully killed by the server. Will not attempt reconnect. Reason: %@", reason);
+ [self interruptForReason:kFInterruptReasonServerKill];
+}
+
+#pragma mark -
+#pragma mark Connection handling methods
+
+- (void) interruptForReason:(NSString *)reason {
+ FFLog(@"I-RDB034006", @"Connection interrupted for: %@", reason);
+
+ [self.interruptReasons addObject:reason];
+ if (self.realtime) {
+ // Will call onDisconnect and set the connection state to Disconnected
+ [self.realtime close];
+ self.realtime = nil;
+ } else {
+ [self.retryHelper cancel];
+ self->connectionState = ConnectionStateDisconnected;
+ }
+ // Reset timeouts
+ [self.retryHelper signalSuccess];
+}
+
+- (void) resumeForReason:(NSString *)reason {
+ FFLog(@"I-RDB034007", @"Connection no longer interrupted for: %@", reason);
+ [self.interruptReasons removeObject:reason];
+
+ if ([self shouldReconnect] && connectionState == ConnectionStateDisconnected) {
+ [self tryScheduleReconnect];
+ }
+}
+
+- (BOOL) shouldReconnect {
+ return self.interruptReasons.count == 0;
+}
+
+- (BOOL) isInterruptedForReason:(NSString *)reason {
+ return [self.interruptReasons containsObject:reason];
+}
+
+#pragma mark -
+#pragma mark Private methods
+
+- (void) tryScheduleReconnect {
+ if ([self shouldReconnect]) {
+ NSAssert(self->connectionState == ConnectionStateDisconnected,
+ @"Not in disconnected state: %d", self->connectionState);
+ BOOL forceRefresh = self.forceAuthTokenRefresh;
+ self.forceAuthTokenRefresh = NO;
+ FFLog(@"I-RDB034008", @"Scheduling connection attempt");
+ [self.retryHelper retry:^{
+ FFLog(@"I-RDB034009", @"Trying to fetch auth token");
+ NSAssert(self->connectionState == ConnectionStateDisconnected,
+ @"Not in disconnected state: %d", self->connectionState);
+ self->connectionState = ConnectionStateGettingToken;
+ self.currentFetchTokenAttempt++;
+ NSUInteger thisFetchTokenAttempt = self.currentFetchTokenAttempt;
+ [self.authTokenProvider fetchTokenForcingRefresh:forceRefresh withCallback:^(NSString *token, NSError *error) {
+ if (thisFetchTokenAttempt == self.currentFetchTokenAttempt) {
+ if (error != nil) {
+ self->connectionState = ConnectionStateDisconnected;
+ FFLog(@"I-RDB034010", @"Error fetching token: %@", error);
+ [self tryScheduleReconnect];
+ } else {
+ // Someone could have interrupted us while fetching the token,
+ // marking the connection as Disconnected
+ if (self->connectionState == ConnectionStateGettingToken) {
+ FFLog(@"I-RDB034011", @"Successfully fetched token, opening connection");
+ [self openNetworkConnectionWithToken:token];
+ } else {
+ NSAssert(self->connectionState == ConnectionStateDisconnected,
+ @"Expected connection state disconnected, but got %d", self->connectionState);
+ FFLog(@"I-RDB034012", @"Not opening connection after token refresh, because connection was set to disconnected.");
+ }
+ }
+ } else {
+ FFLog(@"I-RDB034013", @"Ignoring fetch token result, because this was not the latest attempt.");
+ }
+ }];
+ }];
+
+ }
+}
+
+- (void) openNetworkConnectionWithToken:(NSString *)token {
+ NSAssert(self->connectionState == ConnectionStateGettingToken,
+ @"Trying to open network connection while in wrong state: %d", self->connectionState);
+ self.authToken = token;
+ self->connectionState = ConnectionStateConnecting;
+ self.realtime = [[FConnection alloc] initWith:self.repoInfo
+ andDispatchQueue:self.dispatchQueue
+ lastSessionID:self.lastSessionID];
+ self.realtime.delegate = self;
+ [self.realtime open];
+}
+
+static void reachabilityCallback(SCNetworkReachabilityRef ref, SCNetworkReachabilityFlags flags, void* info) {
+ if (flags & kSCNetworkReachabilityFlagsReachable) {
+ FFLog(@"I-RDB034014", @"Network became reachable. Trigger a connection attempt");
+ FPersistentConnection* self = (__bridge FPersistentConnection *)info;
+ // Reset reconnect delay
+ [self.retryHelper signalSuccess];
+ if (self->connectionState == ConnectionStateDisconnected) {
+ [self tryScheduleReconnect];
+ }
+ } else {
+ FFLog(@"I-RDB034015", @"Network is not reachable");
+ }
+}
+
+- (void) enteringForeground {
+ dispatch_async(self.dispatchQueue, ^{
+ // Reset reconnect delay
+ [self.retryHelper signalSuccess];
+ if (self->connectionState == ConnectionStateDisconnected) {
+ [self tryScheduleReconnect];
+ }
+ });
+}
+
+- (void) setupNotifications {
+
+ NSString * const* foregroundConstant = (NSString * const *) dlsym(RTLD_DEFAULT, "UIApplicationWillEnterForegroundNotification");
+ if (foregroundConstant) {
+ [[NSNotificationCenter defaultCenter] addObserver:self
+ selector:@selector(enteringForeground)
+ name:*foregroundConstant
+ object:nil];
+ }
+ // An empty address is interpreted a generic internet access
+ struct sockaddr_in zeroAddress;
+ bzero(&zeroAddress, sizeof(zeroAddress));
+ zeroAddress.sin_len = sizeof(zeroAddress);
+ zeroAddress.sin_family = AF_INET;
+ reachability = SCNetworkReachabilityCreateWithAddress(kCFAllocatorDefault, (const struct sockaddr *)&zeroAddress);
+ SCNetworkReachabilityContext ctx = {0, (__bridge void *)(self), NULL, NULL, NULL};
+ if (SCNetworkReachabilitySetCallback(reachability, reachabilityCallback, &ctx)) {
+ SCNetworkReachabilitySetDispatchQueue(reachability, self.dispatchQueue);
+ } else {
+ FFLog(@"I-RDB034016", @"Failed to set up network reachability monitoring");
+ CFRelease(reachability);
+ reachability = NULL;
+ }
+}
+
+- (void) sendAuthAndRestoreStateAfterComplete:(BOOL)restoreStateAfterComplete {
+ NSAssert([self connected], @"Must be connected to send auth");
+ NSAssert(self.authToken != nil, @"Can't send auth if there is no credential");
+
+ NSDictionary* requestData = @{kFWPRequestCredential: self.authToken};
+ [self sendAction:kFWPRequestActionAuth body:requestData sensitive:YES callback:^(NSDictionary *data) {
+ self->connectionState = ConnectionStateConnected;
+ NSString* status = [data objectForKey:kFWPResponseForActionStatus];
+ id responseData = [data objectForKey:kFWPResponseForActionData];
+ if (responseData == nil) {
+ responseData = @"error";
+ }
+
+ BOOL statusOk = [status isEqualToString:kFWPResponseForActionStatusOk];
+ if (statusOk) {
+ if (restoreStateAfterComplete) {
+ [self restoreState];
+ }
+ } else {
+ self.authToken = nil;
+ self.forceAuthTokenRefresh = YES;
+ if ([status isEqualToString:@"expired_token"]) {
+ FFLog(@"I-RDB034017", @"Authentication failed: %@ (%@)", status, responseData);
+ } else {
+ FFWarn(@"I-RDB034018", @"Authentication failed: %@ (%@)", status, responseData);
+ }
+ [self.realtime close];
+ }
+ }];
+}
+
+- (void) sendUnauth {
+ [self sendAction:kFWPRequestActionUnauth body:@{} sensitive:NO callback:nil];
+}
+
+- (void) onAuthRevokedWithStatus:(NSString *)status andReason:(NSString *)reason {
+ // This might be for an earlier token than we just recently sent. But since we need to close the connection anyways,
+ // we can set it to null here and we will refresh the token later on reconnect
+ if ([status isEqualToString:@"expired_token"]) {
+ FFLog(@"I-RDB034019", @"Auth token revoked: %@ (%@)", status, reason);
+ } else {
+ FFWarn(@"I-RDB034020", @"Auth token revoked: %@ (%@)", status, reason);
+ }
+ self.authToken = nil;
+ self.forceAuthTokenRefresh = YES;
+ // Try reconnecting on auth revocation
+ [self.realtime close];
+}
+
+- (void) onListenRevoked:(FPath *)path {
+ NSArray *queries = [self removeAllListensAtPath:path];
+ for (FOutstandingQuery* query in queries) {
+ query.onComplete(@"permission_denied");
+ }
+}
+
+- (void) sendOnDisconnectAction:(NSString *)action forPath:(NSString *)pathString withData:(id)data andCallback:(fbt_void_nsstring_nsstring)callback {
+
+ NSDictionary* request = @{kFWPRequestPath: pathString, kFWPRequestData: data};
+ FFLog(@"I-RDB034021", @"onDisconnect %@: %@", action, request);
+
+ [self sendAction:action
+ body:request
+ sensitive:NO
+ callback:^(NSDictionary *data) {
+ NSString* status = [data objectForKey:kFWPResponseForActionStatus];
+ NSString* errorReason = [data objectForKey:kFWPResponseForActionData];
+ callback(status, errorReason);
+ }];
+}
+
+- (void) sendPut:(NSNumber *) index {
+ NSAssert([self canSendWrites], @"sendPut called when not able to send writes");
+ FOutstandingPut* put = self.outstandingPuts[index];
+ assert(put != nil);
+ fbt_void_nsstring_nsstring onComplete = put.onCompleteBlock;
+
+ // Do not async this block; copying the block insinde sendAction: doesn't happen in time (or something) so coredumps
+ put.sent = YES;
+ [self sendAction:put.action
+ body:put.request
+ sensitive:NO
+ callback:^(NSDictionary* data) {
+
+ FOutstandingPut *currentPut = self.outstandingPuts[index];
+ if (currentPut == put) {
+ [self.outstandingPuts removeObjectForKey:index];
+
+ if (onComplete != nil) {
+ NSString *status = [data objectForKey:kFWPResponseForActionStatus];
+ NSString *errorReason = [data objectForKey:kFWPResponseForActionData];
+ if (self.unackedListensCount == 0) {
+ onComplete(status, errorReason);
+ } else {
+ FTupleCallbackStatus *putToAck = [[FTupleCallbackStatus alloc] init];
+ putToAck.block = onComplete;
+ putToAck.status = status;
+ putToAck.errorReason = errorReason;
+ [self.putsToAck addObject:putToAck];
+ }
+ }
+ } else {
+ FFLog(@"I-RDB034022", @"Ignoring on complete for put %@ because it was already removed", index);
+ }
+ }];
+}
+
+- (void) sendUnlisten:(FPath *)path queryParams:(FQueryParams *)queryParams tagId:(NSNumber *)tagId {
+ FFLog(@"I-RDB034023", @"Unlisten on %@ for %@", path, queryParams);
+
+ NSMutableDictionary* request = [NSMutableDictionary dictionaryWithObjectsAndKeys:[path toString], kFWPRequestPath, nil];
+ if (tagId) {
+ [request setObject:queryParams.wireProtocolParams forKey:kFWPRequestQueries];
+ [request setObject:tagId forKey:kFWPRequestTag];
+ }
+
+ [self sendAction:kFWPRequestActionTaggedUnlisten
+ body:request
+ sensitive:NO
+ callback:nil];
+}
+
+- (void) putInternal:(id)data forAction:(NSString *)action forPath:(NSString *)pathString withHash:(NSString *)hash withCallback:(fbt_void_nsstring_nsstring)onComplete {
+
+ NSMutableDictionary *request = [NSMutableDictionary dictionaryWithObjectsAndKeys:
+ pathString, kFWPRequestPath,
+ data, kFWPRequestData, nil];
+ if(hash) {
+ [request setObject:hash forKey:kFWPRequestHash];
+ }
+
+ FOutstandingPut *put = [[FOutstandingPut alloc] init];
+ put.action = action;
+ put.request = request;
+ put.onCompleteBlock = onComplete;
+ put.sent = NO;
+
+ NSNumber* index = [self.putCounter getAndIncrement];
+ self.outstandingPuts[index] = put;
+
+ if ([self canSendWrites]) {
+ FFLog(@"I-RDB034024", @"Was connected, and added as index: %@", index);
+ [self sendPut:index];
+ }
+ else {
+ FFLog(@"I-RDB034025", @"Wasn't connected or writes paused, so added to outstanding puts only. Path: %@", pathString);
+ }
+}
+
+- (void) sendListen:(FOutstandingQuery *)listenSpec {
+ FQuerySpec *query = listenSpec.query;
+ FFLog(@"I-RDB034026", @"Listen for %@", query);
+ NSMutableDictionary *request = [NSMutableDictionary dictionaryWithObject:[query.path toString] forKey:kFWPRequestPath];
+
+ // Only bother to send query if it's non-default
+ if (listenSpec.tagId != nil) {
+ [request setObject:[query.params wireProtocolParams] forKey:kFWPRequestQueries];
+ [request setObject:listenSpec.tagId forKey:kFWPRequestTag];
+ }
+
+ [request setObject:[listenSpec.syncTreeHash simpleHash] forKey:kFWPRequestHash];
+ if ([listenSpec.syncTreeHash includeCompoundHash]) {
+ FCompoundHash *compoundHash = [listenSpec.syncTreeHash compoundHash];
+ NSMutableArray *posts = [NSMutableArray array];
+ for (FPath *path in compoundHash.posts) {
+ [posts addObject:path.wireFormat];
+ }
+ request[kFWPRequestCompoundHash] = @{ kFWPRequestCompoundHashHashes: compoundHash.hashes,
+ kFWPRequestCompoundHashPaths: posts };
+ }
+
+ fbt_void_nsdictionary onResponse = ^(NSDictionary *response) {
+ FFLog(@"I-RDB034027", @"Listen response %@", response);
+ // warn in any case, even if the listener was removed
+ [self warnOnListenWarningsForQuery:query payload:response[kFWPResponseForActionData]];
+
+ FOutstandingQuery *currentListenSpec = self.listens[query];
+
+ // only trigger actions if the listen hasn't been removed (and maybe readded)
+ if (currentListenSpec == listenSpec) {
+ NSString *status = [response objectForKey:kFWPRequestStatus];
+ if (![status isEqualToString:@"ok"]) {
+ [self removeListen:query];
+ }
+
+ if (listenSpec.onComplete) {
+ listenSpec.onComplete(status);
+ }
+ }
+
+ self.unackedListensCount--;
+ NSAssert(self.unackedListensCount >= 0, @"unackedListensCount decremented to be negative.");
+ if (self.unackedListensCount == 0) {
+ [self ackPuts];
+ }
+ };
+
+ [self sendAction:kFWPRequestActionTaggedListen
+ body:request
+ sensitive:NO
+ callback:onResponse];
+
+ self.unackedListensCount++;
+}
+
+- (void) warnOnListenWarningsForQuery:(FQuerySpec *)query payload:(id)payload {
+ if (payload != nil && [payload isKindOfClass:[NSDictionary class]]) {
+ NSDictionary *payloadDict = payload;
+ id warnings = payloadDict[kFWPResponseDataWarnings];
+ if (warnings != nil && [warnings isKindOfClass:[NSArray class]]) {
+ NSArray *warningsArr = warnings;
+ if ([warningsArr containsObject:@"no_index"]) {
+ NSString *indexSpec = [NSString stringWithFormat:@"\".indexOn\": \"%@\"", [query.params.index queryDefinition]];
+ NSString *indexPath = [query.path description];
+ FFWarn(@"I-RDB034028", @"Using an unspecified index. Consider adding %@ at %@ to your security rules for better performance", indexSpec, indexPath);
+ }
+ }
+ }
+}
+
+- (int) getNextRequestNumber {
+ return [[self.requestNumber getAndIncrement] intValue];
+}
+
+- (void)sendAction:(NSString *)action
+ body:(NSDictionary *)message
+ sensitive:(BOOL)sensitive
+ callback:(void (^)(NSDictionary* data))onMessage {
+ // Hold onto the onMessage callback for this request before firing it off
+ NSNumber* rn = [NSNumber numberWithInt:[self getNextRequestNumber]];
+ NSDictionary* msg = [NSDictionary dictionaryWithObjectsAndKeys:
+ rn, kFWPRequestNumber,
+ action, kFWPRequestAction,
+ message, kFWPRequestPayloadBody,
+ nil];
+
+ [self.realtime sendRequest:msg sensitive:sensitive];
+
+ if (onMessage) {
+ // Debug message without a callback; bump the rn, but don't hold onto the cb
+ [self.requestCBHash setObject:[onMessage copy] forKey:rn];
+ }
+}
+
+- (void) cancelSentTransactions {
+ NSMutableArray* toPrune = [[NSMutableArray alloc] init];
+ for (NSNumber* index in self.outstandingPuts) {
+ FOutstandingPut* put = self.outstandingPuts[index];
+ if (put.request[kFWPRequestHash] && put.sent) {
+ // This is a sent transaction put
+ put.onCompleteBlock(kFTransactionDisconnect, @"Client was disconnected while running a transaction");
+ [toPrune addObject:index];
+ }
+ }
+ for (NSNumber* index in toPrune) {
+ [self.outstandingPuts removeObjectForKey:index];
+ }
+}
+
+- (void) onDataPushWithAction:(NSString *)action andBody:(NSDictionary *)body {
+ FFLog(@"I-RDB034029", @"handleServerMessage: %@, %@", action, body);
+ id<FPersistentConnectionDelegate> delegate = self.delegate;
+ if ([action isEqualToString:kFWPAsyncServerDataUpdate] || [action isEqualToString:kFWPAsyncServerDataMerge]) {
+ BOOL isMerge = [action isEqualToString:kFWPAsyncServerDataMerge];
+
+ if ([body objectForKey:kFWPAsyncServerDataUpdateBodyPath] && [body objectForKey:kFWPAsyncServerDataUpdateBodyData]) {
+ NSString* path = [body objectForKey:kFWPAsyncServerDataUpdateBodyPath];
+ id payloadData = [body objectForKey:kFWPAsyncServerDataUpdateBodyData];
+ if (isMerge && [payloadData isKindOfClass:[NSDictionary class]] && [payloadData count] == 0) {
+ // ignore empty merge
+ } else {
+ [delegate onDataUpdate:self forPath:path message:payloadData isMerge:isMerge tagId:[body objectForKey:kFWPAsyncServerDataUpdateBodyTag]];
+ }
+ }
+ else {
+ FFLog(@"I-RDB034030", @"Malformed data response from server missing path or data: %@", body);
+ }
+ } else if ([action isEqualToString:kFWPAsyncServerDataRangeMerge]) {
+ NSString *path = body[kFWPAsyncServerDataUpdateBodyPath];
+ NSArray *ranges = body[kFWPAsyncServerDataUpdateBodyData];
+ NSNumber *tag = body[kFWPAsyncServerDataUpdateBodyTag];
+ NSMutableArray *rangeMerges = [NSMutableArray array];
+ for (NSDictionary *range in ranges) {
+ NSString *startString = range[kFWPAsyncServerDataUpdateStartPath];
+ NSString *endString = range[kFWPAsyncServerDataUpdateEndPath];
+ id updateData = range[kFWPAsyncServerDataUpdateRangeMerge];
+ id<FNode> updates = [FSnapshotUtilities nodeFrom:updateData];
+ FPath *start = (startString != nil) ? [[FPath alloc] initWith:startString] : nil;
+ FPath *end = (endString != nil) ? [[FPath alloc] initWith:endString] : nil;
+ FRangeMerge *merge = [[FRangeMerge alloc] initWithStart:start end:end updates:updates];
+ [rangeMerges addObject:merge];
+ }
+ [delegate onRangeMerge:rangeMerges forPath:path tagId:tag];
+ } else if ([action isEqualToString:kFWPAsyncServerAuthRevoked]) {
+ NSString* status = [body objectForKey:kFWPResponseForActionStatus];
+ NSString* reason = [body objectForKey:kFWPResponseForActionData];
+ [self onAuthRevokedWithStatus:status andReason:reason];
+ } else if ([action isEqualToString:kFWPASyncServerListenCancelled]) {
+ NSString* pathString = [body objectForKey:kFWPAsyncServerDataUpdateBodyPath];
+ [self onListenRevoked:[[FPath alloc] initWith:pathString]];
+ } else if ([action isEqualToString:kFWPAsyncServerSecurityDebug]) {
+ NSString* msg = [body objectForKey:@"msg"];
+ if (msg != nil) {
+ NSArray *msgs = [msg componentsSeparatedByString:@"\n"];
+ for (NSString* m in msgs) {
+ FFWarn(@"I-RDB034031", @"%@", m);
+ }
+ }
+ } else {
+ // TODO: revoke listens, auth, security debug
+ FFLog(@"I-RDB034032", @"Unsupported action from server: %@", action);
+ }
+}
+
+- (void) restoreAuth {
+ FFLog(@"I-RDB034033", @"Calling restore state");
+
+ NSAssert(self->connectionState == ConnectionStateConnecting,
+ @"Wanted to restore auth, but was in wrong state: %d", self->connectionState);
+ if (self.authToken == nil) {
+ FFLog(@"I-RDB034034", @"Not restoring auth because token is nil");
+ self->connectionState = ConnectionStateConnected;
+ [self restoreState];
+ } else {
+ FFLog(@"I-RDB034035", @"Restoring auth");
+ self->connectionState = ConnectionStateAuthenticating;
+ [self sendAuthAndRestoreStateAfterComplete:YES];
+ }
+}
+
+- (void) restoreState {
+ NSAssert(self->connectionState == ConnectionStateConnected,
+ @"Should be connected if we're restoring state, but we are: %d", self->connectionState);
+
+ [self.listens enumerateKeysAndObjectsUsingBlock:^(FQuerySpec *query, FOutstandingQuery *outstandingListen, BOOL *stop) {
+ FFLog(@"I-RDB034036", @"Restoring listen for %@", query);
+ [self sendListen:outstandingListen];
+ }];
+
+ NSArray* keys = [[self.outstandingPuts allKeys] sortedArrayUsingSelector:@selector(compare:)];
+ for(int i = 0; i < [keys count]; i++) {
+ if([self.outstandingPuts objectForKey:[keys objectAtIndex:i]] != nil) {
+ FFLog(@"I-RDB034037", @"Restoring put: %d", i);
+ [self sendPut:[keys objectAtIndex:i]];
+ }
+ else {
+ FFLog(@"I-RDB034038", @"Restoring put: skipped nil: %d", i);
+ }
+ }
+
+ for (FTupleOnDisconnect* tuple in self.onDisconnectQueue) {
+ [self sendOnDisconnectAction:tuple.action forPath:tuple.pathString withData:tuple.data andCallback:tuple.onComplete];
+ }
+ [self.onDisconnectQueue removeAllObjects];
+}
+
+- (NSArray *) removeListen:(FQuerySpec *)query {
+ NSAssert(query.isDefault || !query.loadsAllData, @"removeListen called for non-default but complete query");
+
+ FOutstandingQuery* outstanding = self.listens[query];
+ if (!outstanding) {
+ FFLog(@"I-RDB034039", @"Trying to remove listener for query %@ but no listener exists", query);
+ return @[];
+ } else {
+ [self.listens removeObjectForKey:query];
+ return @[outstanding];
+ }
+}
+
+- (NSArray *) removeAllListensAtPath:(FPath *)path {
+ FFLog(@"I-RDB034040", @"Removing all listens at path %@", path);
+ NSMutableArray *removed = [NSMutableArray array];
+ NSMutableArray *toRemove = [NSMutableArray array];
+ [self.listens enumerateKeysAndObjectsUsingBlock:^(FQuerySpec *spec, FOutstandingQuery *outstanding, BOOL *stop) {
+ if ([spec.path isEqual:path]) {
+ [removed addObject:outstanding];
+ [toRemove addObject:spec];
+ }
+ }];
+ [self.listens removeObjectsForKeys:toRemove];
+ return removed;
+}
+
+- (void) purgeOutstandingWrites {
+ // We might have unacked puts in our queue that we need to ack now before we send out any cancels...
+ [self ackPuts];
+ // Cancel in order
+ NSArray* keys = [[self.outstandingPuts allKeys] sortedArrayUsingSelector:@selector(compare:)];
+ for (NSNumber *key in keys) {
+ FOutstandingPut *put = self.outstandingPuts[key];
+ if (put.onCompleteBlock != nil) {
+ put.onCompleteBlock(kFErrorWriteCanceled, nil);
+ }
+ }
+ for (FTupleOnDisconnect *onDisconnect in self.onDisconnectQueue) {
+ if (onDisconnect.onComplete != nil) {
+ onDisconnect.onComplete(kFErrorWriteCanceled, nil);
+ }
+ }
+ [self.outstandingPuts removeAllObjects];
+ [self.onDisconnectQueue removeAllObjects];
+}
+
+- (void) ackPuts {
+ for (FTupleCallbackStatus *put in self.putsToAck) {
+ put.block(put.status, put.errorReason);
+ }
+ [self.putsToAck removeAllObjects];
+}
+
+- (void) handleTimestamp:(NSNumber *)timestamp {
+ FFLog(@"I-RDB034041", @"Handling timestamp: %@", timestamp);
+ double timestampDeltaMs = [timestamp doubleValue] - ([[NSDate date] timeIntervalSince1970] * 1000);
+ [self.delegate onServerInfoUpdate:self updates:@{kDotInfoServerTimeOffset: [NSNumber numberWithDouble:timestampDeltaMs]}];
+}
+
+- (void) sendStats:(NSDictionary *)stats {
+ if ([stats count] > 0) {
+ NSDictionary *request = @{ kFWPRequestCounters: stats };
+ [self sendAction:kFWPRequestActionStats body:request sensitive:NO callback:^(NSDictionary *data) {
+ NSString* status = [data objectForKey:kFWPResponseForActionStatus];
+ NSString* errorReason = [data objectForKey:kFWPResponseForActionData];
+ BOOL statusOk = [status isEqualToString:kFWPResponseForActionStatusOk];
+ if (!statusOk) {
+ FFLog(@"I-RDB034042", @"Failed to send stats: %@", errorReason);
+ }
+ }];
+ } else {
+ FFLog(@"I-RDB034043", @"Not sending stats because stats are empty");
+ }
+}
+
+- (void) sendConnectStats {
+ NSMutableDictionary *stats = [NSMutableDictionary dictionary];
+
+#if TARGET_OS_IPHONE
+ if (self.config.persistenceEnabled) {
+ stats[@"persistence.ios.enabled"] = @1;
+ }
+#else // this must be OSX then
+ if (self.config.persistenceEnabled) {
+ stats[@"persistence.osx.enabled"] = @1;
+ }
+#endif
+ NSString *sdkVersion = [[FIRDatabase sdkVersion] stringByReplacingOccurrencesOfString:@"." withString:@"-"];
+ NSString *sdkStatName = [NSString stringWithFormat:@"sdk.objc.%@", sdkVersion];
+ stats[sdkStatName] = @1;
+ FFLog(@"I-RDB034044", @"Sending first connection stats");
+ [self sendStats:stats];
+}
+
+- (NSDictionary *) dumpListens {
+ return self.listens;
+}
+
+@end
diff --git a/Firebase/Database/Core/FQueryParams.h b/Firebase/Database/Core/FQueryParams.h
new file mode 100644
index 0000000..e9728e7
--- /dev/null
+++ b/Firebase/Database/Core/FQueryParams.h
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+@protocol FIndex, FNodeFilter, FNode;
+
+@interface FQueryParams : NSObject <NSCopying>
+
+@property (nonatomic, readonly) BOOL limitSet;
+@property (nonatomic, readonly) NSInteger limit;
+
+@property (nonatomic, strong, readonly) NSString *viewFrom;
+@property (nonatomic, strong, readonly) id<FNode> indexStartValue;
+@property (nonatomic, strong, readonly) NSString *indexStartKey;
+@property (nonatomic, strong, readonly) id<FNode> indexEndValue;
+@property (nonatomic, strong, readonly) NSString *indexEndKey;
+
+@property (nonatomic, strong, readonly) id<FIndex> index;
+
+- (BOOL)loadsAllData;
+- (BOOL)isDefault;
+- (BOOL)isValid;
+- (BOOL)hasAnchoredLimit;
+
+- (FQueryParams *) limitTo:(NSInteger) limit;
+- (FQueryParams *) limitToFirst:(NSInteger) newLimit;
+- (FQueryParams *) limitToLast:(NSInteger) newLimit;
+
+- (FQueryParams *) startAt:(id<FNode>)indexValue childKey:(NSString *)key;
+- (FQueryParams *) startAt:(id<FNode>)indexValue;
+- (FQueryParams *) endAt:(id<FNode>)indexValue childKey:(NSString *)key;
+- (FQueryParams *) endAt:(id<FNode>)indexValue;
+
+- (FQueryParams *) orderBy:(id<FIndex>) index;
+
++ (FQueryParams *) defaultInstance;
++ (FQueryParams *) fromQueryObject:(NSDictionary *)dict;
+
+- (BOOL)hasStart;
+- (BOOL)hasEnd;
+
+- (NSDictionary *) wireProtocolParams;
+- (BOOL) isViewFromLeft;
+- (id<FNodeFilter>) nodeFilter;
+@end
diff --git a/Firebase/Database/Core/FQueryParams.m b/Firebase/Database/Core/FQueryParams.m
new file mode 100644
index 0000000..7920358
--- /dev/null
+++ b/Firebase/Database/Core/FQueryParams.m
@@ -0,0 +1,372 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FQueryParams.h"
+#import "FValidation.h"
+#import "FConstants.h"
+#import "FIndex.h"
+#import "FPriorityIndex.h"
+#import "FUtilities.h"
+#import "FNodeFilter.h"
+#import "FIndexedFilter.h"
+#import "FLimitedFilter.h"
+#import "FRangedFilter.h"
+#import "FNode.h"
+#import "FSnapshotUtilities.h"
+
+@interface FQueryParams ()
+
+@property (nonatomic, readwrite) BOOL limitSet;
+@property (nonatomic, readwrite) NSInteger limit;
+
+@property (nonatomic, strong, readwrite) NSString *viewFrom;
+/**
+* indexStartValue is anything you can store as a priority / value.
+*/
+@property (nonatomic, strong, readwrite) id<FNode> indexStartValue;
+@property (nonatomic, strong, readwrite) NSString *indexStartKey;
+/**
+* indexStartValue is anything you can store as a priority / value.
+*/
+@property (nonatomic, strong, readwrite) id<FNode> indexEndValue;
+@property (nonatomic, strong, readwrite) NSString *indexEndKey;
+
+@property (nonatomic, strong, readwrite) id<FIndex> index;
+
+@end
+
+@implementation FQueryParams
+
++ (FQueryParams *) defaultInstance {
+ static FQueryParams *defaultParams = nil;
+ static dispatch_once_t defaultParamsToken;
+ dispatch_once(&defaultParamsToken, ^{
+ defaultParams = [[FQueryParams alloc] init];
+ });
+ return defaultParams;
+}
+
+
+- (id)init {
+ self = [super init];
+ if (self) {
+ self->_limitSet = NO;
+ self->_limit = 0;
+
+ self->_viewFrom = nil;
+ self->_indexStartValue = nil;
+ self->_indexStartKey = nil;
+ self->_indexEndValue = nil;
+ self->_indexEndKey = nil;
+
+ self->_index = [FPriorityIndex priorityIndex];
+ }
+ return self;
+}
+
+/**
+* Only valid if hasStart is true
+*/
+- (id) indexStartValue {
+ NSAssert([self hasStart], @"Only valid if start has been set");
+ return _indexStartValue;
+}
+
+/**
+* Only valid if hasStart is true.
+* @return The starting key name for the range defined by these query parameters
+*/
+- (NSString *) indexStartKey {
+ NSAssert([self hasStart], @"Only valid if start has been set");
+ if (_indexStartKey == nil) {
+ return [FUtilities minName];
+ } else {
+ return _indexStartKey;
+ }
+}
+
+/**
+* Only valid if hasEnd is true.
+*/
+- (id) indexEndValue {
+ NSAssert([self hasEnd], @"Only valid if end has been set");
+ return _indexEndValue;
+}
+
+/**
+* Only valid if hasEnd is true.
+* @return The end key name for the range defined by these query parameters
+*/
+- (NSString *) indexEndKey {
+ NSAssert([self hasEnd], @"Only valid if end has been set");
+ if (_indexEndKey == nil) {
+ return [FUtilities maxName];
+ } else {
+ return _indexEndKey;
+ }
+}
+
+/**
+* @return true if a limit has been set and has been explicitly anchored
+*/
+- (BOOL) hasAnchoredLimit {
+ return self.limitSet && self.viewFrom != nil;
+}
+
+/**
+* Only valid to call if limitSet returns true
+*/
+- (NSInteger) limit {
+ NSAssert(self.limitSet, @"Only valid if limit has been set");
+ return _limit;
+}
+
+- (BOOL)hasStart {
+ return self->_indexStartValue != nil;
+}
+
+- (BOOL)hasEnd {
+ return self->_indexEndValue != nil;
+}
+
+- (id) copyWithZone:(NSZone *)zone {
+ // Immutable
+ return self;
+}
+
+- (id) mutableCopy {
+ FQueryParams* other = [[[self class] alloc] init];
+ // Maybe need to do extra copying here
+ other->_limitSet = _limitSet;
+ other->_limit = _limit;
+ other->_indexStartValue = _indexStartValue;
+ other->_indexStartKey = _indexStartKey;
+ other->_indexEndValue = _indexEndValue;
+ other->_indexEndKey = _indexEndKey;
+ other->_viewFrom = _viewFrom;
+ other->_index = _index;
+ return other;
+}
+
+- (FQueryParams *) limitTo:(NSInteger)newLimit {
+ FQueryParams *newParams = [self mutableCopy];
+ newParams->_limitSet = YES;
+ newParams->_limit = newLimit;
+ newParams->_viewFrom = nil;
+ return newParams;
+}
+
+- (FQueryParams *) limitToFirst:(NSInteger)newLimit {
+ FQueryParams *newParams = [self mutableCopy];
+ newParams->_limitSet = YES;
+ newParams->_limit = newLimit;
+ newParams->_viewFrom = kFQPViewFromLeft;
+ return newParams;
+}
+
+- (FQueryParams *) limitToLast:(NSInteger)newLimit {
+ FQueryParams *newParams = [self mutableCopy];
+ newParams->_limitSet = YES;
+ newParams->_limit = newLimit;
+ newParams->_viewFrom = kFQPViewFromRight;
+ return newParams;
+}
+
+- (FQueryParams *) startAt:(id<FNode>)indexValue childKey:(NSString *)key {
+ NSAssert([indexValue isLeafNode] || [indexValue isEmpty], nil);
+ FQueryParams *newParams = [self mutableCopy];
+ newParams->_indexStartValue = indexValue;
+ newParams->_indexStartKey = key;
+ return newParams;
+}
+
+- (FQueryParams *) startAt:(id<FNode>)indexValue {
+ return [self startAt:indexValue childKey:nil];
+}
+
+- (FQueryParams *) endAt:(id<FNode>)indexValue childKey:(NSString *)key {
+ NSAssert([indexValue isLeafNode] || [indexValue isEmpty], nil);
+ FQueryParams *newParams = [self mutableCopy];
+ newParams->_indexEndValue = indexValue;
+ newParams->_indexEndKey = key;
+ return newParams;
+}
+
+- (FQueryParams *) endAt:(id<FNode>)indexValue {
+ return [self endAt:indexValue childKey:nil];
+}
+
+- (FQueryParams *) orderBy:(id)newIndex {
+ FQueryParams *newParams = [self mutableCopy];
+ newParams->_index = newIndex;
+ return newParams;
+}
+
+- (NSDictionary *) wireProtocolParams {
+ NSMutableDictionary* dict = [[NSMutableDictionary alloc] init];
+ if ([self hasStart]) {
+ [dict setObject:[self.indexStartValue valForExport:YES] forKey:kFQPIndexStartValue];
+
+ // Don't use property as it will be [MIN-NAME]
+ if (self->_indexStartKey != nil) {
+ [dict setObject:self->_indexStartKey forKey:kFQPIndexStartName];
+ }
+ }
+
+ if ([self hasEnd]) {
+ [dict setObject:[self.indexEndValue valForExport:YES] forKey:kFQPIndexEndValue];
+
+ // Don't use property as it will be [MAX-NAME]
+ if (self->_indexEndKey != nil) {
+ [dict setObject:self->_indexEndKey forKey:kFQPIndexEndName];
+ }
+ }
+
+ if (self.limitSet) {
+ [dict setObject:[NSNumber numberWithInteger:self.limit] forKey:kFQPLimit];
+ NSString *vf = self.viewFrom;
+ if (vf == nil) {
+ // limit() rather than limitToFirst or limitToLast was called.
+ // This means that only one of startSet or endSet is true. Use them
+ // to calculate which side of the view to anchor to. If neither is set,
+ // Anchor to end
+ if ([self hasStart]) {
+ vf = kFQPViewFromLeft;
+ } else {
+ vf = kFQPViewFromRight;
+ }
+ }
+ [dict setObject:vf forKey:kFQPViewFrom];
+ }
+
+ // For now, priority index is the default, so we only specify if it's some other index.
+ if (![self.index isEqual:[FPriorityIndex priorityIndex]]) {
+ [dict setObject:[self.index queryDefinition] forKey:kFQPIndex];
+ }
+
+ return dict;
+}
+
++ (FQueryParams *)fromQueryObject:(NSDictionary *)dict {
+ if (dict.count == 0) {
+ return [FQueryParams defaultInstance];
+ }
+
+ FQueryParams *params = [[FQueryParams alloc] init];
+ if (dict[kFQPLimit] != nil) {
+ params->_limitSet = YES;
+ params->_limit = [dict[kFQPLimit] integerValue];
+ }
+
+ if (dict[kFQPIndexStartValue] != nil) {
+ params->_indexStartValue = [FSnapshotUtilities nodeFrom:dict[kFQPIndexStartValue]];
+ if (dict[kFQPIndexStartName] != nil) {
+ params->_indexStartKey = dict[kFQPIndexStartName];
+ }
+ }
+
+ if (dict[kFQPIndexEndValue] != nil) {
+ params->_indexEndValue = [FSnapshotUtilities nodeFrom:dict[kFQPIndexEndValue]];
+ if (dict[kFQPIndexEndName] != nil) {
+ params->_indexEndKey = dict[kFQPIndexEndName];
+ }
+ }
+
+ if (dict[kFQPViewFrom] != nil) {
+ NSString *viewFrom = dict[kFQPViewFrom];
+ if (![viewFrom isEqualToString:kFQPViewFromLeft] && ![viewFrom isEqualToString:kFQPViewFromRight]) {
+ [NSException raise:NSInvalidArgumentException format:@"Unknown view from paramter: %@", viewFrom];
+ }
+ params->_viewFrom = viewFrom;
+ }
+
+ NSString *index = dict[kFQPIndex];
+ if (index != nil) {
+ params->_index = [FIndex indexFromQueryDefinition:index];
+ }
+
+ return params;
+}
+
+- (BOOL) isViewFromLeft {
+ if (self.viewFrom != nil) {
+ // Not null, we can just check
+ return [self.viewFrom isEqualToString:kFQPViewFromLeft];
+ } else {
+ // If start is set, it's view from left. Otherwise not.
+ return self.hasStart;
+ }
+}
+
+- (id<FNodeFilter>) nodeFilter {
+ if (self.loadsAllData) {
+ return [[FIndexedFilter alloc] initWithIndex:self.index];
+ } else if (self.limitSet) {
+ return [[FLimitedFilter alloc] initWithQueryParams:self];
+ } else {
+ return [[FRangedFilter alloc] initWithQueryParams:self];
+ }
+}
+
+
+- (BOOL) isValid {
+ return !(self.hasStart && self.hasEnd && self.limitSet && !self.hasAnchoredLimit);
+}
+
+- (BOOL) loadsAllData {
+ return !(self.hasStart || self.hasEnd || self.limitSet);
+}
+
+- (BOOL) isDefault {
+ return [self loadsAllData] && [self.index isEqual:[FPriorityIndex priorityIndex]];
+}
+
+- (NSString *) description {
+ return [[self wireProtocolParams] description];
+}
+
+- (BOOL) isEqual:(id)obj {
+ if (self == obj) {
+ return YES;
+ }
+ if (![obj isKindOfClass:[self class]]) {
+ return NO;
+ }
+ FQueryParams *other = (FQueryParams *)obj;
+ if (self->_limitSet != other->_limitSet) return NO;
+ if (self->_limit != other->_limit) return NO;
+ if ((self->_index != other->_index) && ![self->_index isEqual:other->_index]) return NO;
+ if ((self->_indexStartKey != other->_indexStartKey) && ![self->_indexStartKey isEqualToString:other->_indexStartKey]) return NO;
+ if ((self->_indexStartValue != other->_indexStartValue) && ![self->_indexStartValue isEqual:other->_indexStartValue]) return NO;
+ if ((self->_indexEndKey != other->_indexEndKey) && ![self->_indexEndKey isEqualToString:other->_indexEndKey]) return NO;
+ if ((self->_indexEndValue != other->_indexEndValue) && ![self->_indexEndValue isEqual:other->_indexEndValue]) return NO;
+ if ([self isViewFromLeft] != [other isViewFromLeft]) return NO;
+
+ return YES;
+}
+
+- (NSUInteger) hash {
+ NSUInteger result = _limitSet ? _limit : 0;
+ result = 31 * result + ([self isViewFromLeft] ? 1231 : 1237);
+ result = 31 * result + [_indexStartKey hash];
+ result = 31 * result + [_indexStartValue hash];
+ result = 31 * result + [_indexEndKey hash];
+ result = 31 * result + [_indexEndValue hash];
+ result = 31 * result + [_index hash];
+ return result;
+}
+
+@end
diff --git a/Firebase/Database/Core/FQuerySpec.h b/Firebase/Database/Core/FQuerySpec.h
new file mode 100644
index 0000000..49ed536
--- /dev/null
+++ b/Firebase/Database/Core/FQuerySpec.h
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+#import "FQueryParams.h"
+#import "FPath.h"
+#import "FIndex.h"
+
+@interface FQuerySpec : NSObject<NSCopying>
+
+@property (nonatomic, strong, readonly) FPath* path;
+@property (nonatomic, strong, readonly) FQueryParams *params;
+
+- (id)initWithPath:(FPath *)path params:(FQueryParams *)params;
+
++ (FQuerySpec *)defaultQueryAtPath:(FPath *)path;
+
+- (id<FIndex>)index;
+- (BOOL)isDefault;
+- (BOOL)loadsAllData;
+
+@end
diff --git a/Firebase/Database/Core/FQuerySpec.m b/Firebase/Database/Core/FQuerySpec.m
new file mode 100644
index 0000000..24be433
--- /dev/null
+++ b/Firebase/Database/Core/FQuerySpec.m
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FQuerySpec.h"
+
+@interface FQuerySpec ()
+
+@property (nonatomic, strong, readwrite) FPath* path;
+@property (nonatomic, strong, readwrite) FQueryParams *params;
+
+
+@end
+
+@implementation FQuerySpec
+
+- (id)initWithPath:(FPath *)path params:(FQueryParams *)params {
+ self = [super init];
+ if (self != nil) {
+ self->_path = path;
+ self->_params = params;
+ }
+ return self;
+}
+
++ (FQuerySpec *)defaultQueryAtPath:(FPath *)path {
+ return [[FQuerySpec alloc] initWithPath:path params:[FQueryParams defaultInstance]];
+}
+
+- (id)copyWithZone:(NSZone *)zone {
+ // Immutable
+ return self;
+}
+
+- (id<FIndex>)index {
+ return self.params.index;
+}
+
+- (BOOL)isDefault {
+ return self.params.isDefault;
+}
+
+- (BOOL)loadsAllData {
+ return self.params.loadsAllData;
+}
+
+- (BOOL)isEqual:(id)object {
+ if (self == object) {
+ return YES;
+ }
+
+ if (![object isKindOfClass:[FQuerySpec class]]) {
+ return NO;
+ }
+
+ FQuerySpec *other = (FQuerySpec *)object;
+
+ if (![self.path isEqual:other.path]) {
+ return NO;
+ }
+
+ return [self.params isEqual:other.params];
+}
+
+- (NSUInteger)hash {
+ return self.path.hash * 31 + self.params.hash;
+}
+
+- (NSString *)description {
+ return [NSString stringWithFormat:@"FQuerySpec (path: %@, params: %@)", self.path, self.params];
+}
+
+@end
diff --git a/Firebase/Database/Core/FRangeMerge.h b/Firebase/Database/Core/FRangeMerge.h
new file mode 100644
index 0000000..8825e0e
--- /dev/null
+++ b/Firebase/Database/Core/FRangeMerge.h
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+#import "FNode.h"
+
+/**
+ * Applies a merge of a snap for a given interval of paths.
+ * Each leaf in the current node which the relative path lies *after* (the optional) start and lies *before or at*
+ * (the optional) end will be deleted. Each leaf in snap that lies in the interval will be added to the resulting node.
+ * Nodes outside of the range are ignored. nil for start and end are sentinel values that represent -infinity and
+ * +infinity respectively (aka includes any path).
+ * Priorities of children nodes are treated as leaf children of that node.
+ */
+@interface FRangeMerge : NSObject
+
+- (instancetype)initWithStart:(FPath *)start end:(FPath *)end updates:(id<FNode>)updates;
+
+- (id<FNode>)applyToNode:(id<FNode>)node;
+
+@end
diff --git a/Firebase/Database/Core/FRangeMerge.m b/Firebase/Database/Core/FRangeMerge.m
new file mode 100644
index 0000000..8bc67bf
--- /dev/null
+++ b/Firebase/Database/Core/FRangeMerge.m
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FRangeMerge.h"
+
+#import "FEmptyNode.h"
+
+@interface FRangeMerge ()
+
+@property (nonatomic, strong) FPath *optExclusiveStart;
+@property (nonatomic, strong) FPath *optInclusiveEnd;
+@property (nonatomic, strong) id<FNode> updates;
+
+@end
+
+@implementation FRangeMerge
+
+- (instancetype)initWithStart:(FPath *)start end:(FPath *)end updates:(id<FNode>)updates {
+ self = [super init];
+ if (self != nil) {
+ self->_optExclusiveStart = start;
+ self->_optInclusiveEnd = end;
+ self->_updates = updates;
+ }
+ return self;
+}
+
+- (id<FNode>)applyToNode:(id<FNode>)node {
+ return [self updateRangeInNode:[FPath empty] node:node updates:self.updates];
+}
+
+- (id<FNode>)updateRangeInNode:(FPath *)currentPath node:(id<FNode>)node updates:(id<FNode>)updates {
+ NSComparisonResult startComparison = (self.optExclusiveStart == nil) ? NSOrderedDescending : [currentPath compare:self.optExclusiveStart];
+ NSComparisonResult endComparison = (self.optInclusiveEnd == nil) ? NSOrderedAscending : [currentPath compare:self.optInclusiveEnd];
+ BOOL startInNode = self.optExclusiveStart != nil && [currentPath contains:self.optExclusiveStart];
+ BOOL endInNode = self.optInclusiveEnd != nil && [currentPath contains:self.optInclusiveEnd];
+ if (startComparison == NSOrderedDescending && endComparison == NSOrderedAscending && !endInNode) {
+ // child is completly contained
+ return updates;
+ } else if (startComparison == NSOrderedDescending && endInNode && [updates isLeafNode]) {
+ return updates;
+ } else if (startComparison == NSOrderedDescending && endComparison == NSOrderedSame) {
+ NSAssert(endInNode, @"End not in node");
+ NSAssert(![updates isLeafNode], @"Found leaf node update, this case should have been handled above.");
+ if ([node isLeafNode]) {
+ // Update node was not a leaf node, so we can delete it
+ return [FEmptyNode emptyNode];
+ } else {
+ // Unaffected by range, ignore
+ return node;
+ }
+ } else if (startInNode || endInNode) {
+ // There is a partial update we need to do, so collect all relevant children
+ NSMutableSet *allChildren = [NSMutableSet set];
+ [node enumerateChildrenUsingBlock:^(NSString *key, id<FNode> node, BOOL *stop) {
+ [allChildren addObject:key];
+ }];
+ [updates enumerateChildrenUsingBlock:^(NSString *key, id<FNode> node, BOOL *stop) {
+ [allChildren addObject:key];
+ }];
+
+ __block id<FNode> newNode = node;
+ void (^action)(id, BOOL *) = ^void(NSString *key, BOOL *stop) {
+ id<FNode> currentChild = [node getImmediateChild:key];
+ id<FNode> updatedChild = [self updateRangeInNode:[currentPath childFromString:key]
+ node:currentChild
+ updates:[updates getImmediateChild:key]];
+ // Only need to update if the node changed
+ if (updatedChild != currentChild) {
+ newNode = [newNode updateImmediateChild:key withNewChild:updatedChild];
+ }
+ };
+
+ [allChildren enumerateObjectsUsingBlock:action];
+
+ // Add priority last, so the node is not empty when applying
+ if (!updates.getPriority.isEmpty || !node.getPriority.isEmpty) {
+ BOOL stop = NO;
+ action(@".priority", &stop);
+ }
+ return newNode;
+ } else {
+ // Unaffected by this range
+ NSAssert(endComparison == NSOrderedDescending || startComparison <= NSOrderedSame, @"Invalid range for update");
+ return node;
+ }
+}
+
+- (NSString *)description {
+ return [NSString stringWithFormat:@"RangeMerge (optExclusiveStart = %@, optExclusiveEng = %@, updates = %@)",
+ self.optExclusiveStart, self.optInclusiveEnd, self.updates];
+}
+
+@end
diff --git a/Firebase/Database/Core/FRepo.h b/Firebase/Database/Core/FRepo.h
new file mode 100644
index 0000000..69ec6bf
--- /dev/null
+++ b/Firebase/Database/Core/FRepo.h
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+#import "FRepoInfo.h"
+#import "FPersistentConnection.h"
+#import "FIRDataEventType.h"
+#import "FTupleUserCallback.h"
+
+@class FQuerySpec;
+@class FPersistence;
+@class FAuthenticationManager;
+@class FIRDatabaseConfig;
+@protocol FEventRegistration;
+@class FCompoundWrite;
+@protocol FClock;
+@class FIRDatabase;
+
+@interface FRepo : NSObject <FPersistentConnectionDelegate>
+
+@property (nonatomic, strong) FIRDatabaseConfig *config;
+
+- (id)initWithRepoInfo:(FRepoInfo *)info config:(FIRDatabaseConfig *)config database:(FIRDatabase *)database;
+
+- (void) set:(FPath *)path withNode:(id)node withCallback:(fbt_void_nserror_ref)onComplete;
+- (void) update:(FPath *)path withNodes:(FCompoundWrite *)compoundWrite withCallback:(fbt_void_nserror_ref)callback;
+- (void) purgeOutstandingWrites;
+
+- (void) addEventRegistration:(id<FEventRegistration>)eventRegistration forQuery:(FQuerySpec *)query;
+- (void) removeEventRegistration:(id<FEventRegistration>)eventRegistration forQuery:(FQuerySpec *)query;
+- (void) keepQuery:(FQuerySpec *)query synced:(BOOL)synced;
+
+- (NSString*)name;
+- (NSTimeInterval)serverTime;
+
+- (void) onDataUpdate:(FPersistentConnection *)fpconnection forPath:(NSString *)pathString message:(id)message isMerge:(BOOL)isMerge tagId:(NSNumber *)tagId;
+- (void) onConnect:(FPersistentConnection *)fpconnection;
+- (void) onDisconnect:(FPersistentConnection *)fpconnection;
+
+// Disconnect methods
+- (void) onDisconnectCancel:(FPath *)path withCallback:(fbt_void_nserror_ref)callback;
+- (void) onDisconnectSet:(FPath *)path withNode:(id<FNode>)node withCallback:(fbt_void_nserror_ref)callback;
+- (void) onDisconnectUpdate:(FPath *)path withNodes:(FCompoundWrite *)compoundWrite withCallback:(fbt_void_nserror_ref)callback;
+
+// Connection Management.
+- (void) interrupt;
+- (void) resume;
+
+// Transactions
+- (void) startTransactionOnPath:(FPath *)path
+ update:(fbt_transactionresult_mutabledata)update
+ onComplete:(fbt_void_nserror_bool_datasnapshot)onComplete
+ withLocalEvents:(BOOL)applyLocally;
+
+// Testing methods
+- (NSDictionary *) dumpListens;
+- (void) dispose;
+- (void) setHijackHash:(BOOL)hijack;
+
+@property (nonatomic, strong, readonly) FAuthenticationManager *auth;
+@property (nonatomic, strong, readonly) FIRDatabase *database;
+
+@end
diff --git a/Firebase/Database/Core/FRepo.m b/Firebase/Database/Core/FRepo.m
new file mode 100644
index 0000000..06cc253
--- /dev/null
+++ b/Firebase/Database/Core/FRepo.m
@@ -0,0 +1,1116 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <dlfcn.h>
+#import "FRepo.h"
+#import "FSnapshotUtilities.h"
+#import "FConstants.h"
+#import "FIRDatabaseQuery_Private.h"
+#import "FQuerySpec.h"
+#import "FTupleNodePath.h"
+#import "FRepo_Private.h"
+#import "FRepoManager.h"
+#import "FServerValues.h"
+#import "FTupleSetIdPath.h"
+#import "FSyncTree.h"
+#import "FEventRegistration.h"
+#import "FAtomicNumber.h"
+#import "FSyncTree.h"
+#import "FListenProvider.h"
+#import "FEventRaiser.h"
+#import "FSnapshotHolder.h"
+#import "FIRDatabaseConfig_Private.h"
+#import "FLevelDBStorageEngine.h"
+#import "FPersistenceManager.h"
+#import "FWriteRecord.h"
+#import "FCachePolicy.h"
+#import "FClock.h"
+#import "FIRDatabase_Private.h"
+#import "FTree.h"
+#import "FTupleTransaction.h"
+#import "FIRTransactionResult.h"
+#import "FIRTransactionResult_Private.h"
+#import "FIRMutableData.h"
+#import "FIRMutableData_Private.h"
+#import "FIRDataSnapshot.h"
+#import "FIRDataSnapshot_Private.h"
+#import "FValueEventRegistration.h"
+#import "FEmptyNode.h"
+
+#ifdef TARGET_OS_IPHONE
+#import <UIKit/UIKit.h>
+#endif
+
+@interface FRepo()
+
+@property (nonatomic, strong) FOffsetClock *serverClock;
+@property (nonatomic, strong) FPersistenceManager* persistenceManager;
+@property (nonatomic, strong) FIRDatabase *database;
+@property (nonatomic, strong, readwrite) FAuthenticationManager *auth;
+@property (nonatomic, strong) FSyncTree *infoSyncTree;
+@property (nonatomic) NSInteger writeIdCounter;
+@property (nonatomic) BOOL hijackHash;
+@property (nonatomic, strong) FTree *transactionQueueTree;
+@property (nonatomic) BOOL loggedTransactionPersistenceWarning;
+
+/**
+* Test only. For load testing the server.
+*/
+@property (nonatomic, strong) id (^interceptServerDataCallback)(NSString *pathString, id data);
+@end
+
+
+@implementation FRepo
+
+- (id)initWithRepoInfo:(FRepoInfo*)info config:(FIRDatabaseConfig *)config database:(FIRDatabase *)database {
+ self = [super init];
+ if (self) {
+ self.repoInfo = info;
+ self.config = config;
+ self.database = database;
+
+ // Access can occur outside of shared queue, so the clock needs to be initialized here
+ self.serverClock = [[FOffsetClock alloc] initWithClock:[FSystemClock clock] offset:0];
+
+ self.connection = [[FPersistentConnection alloc] initWithRepoInfo:self.repoInfo dispatchQueue:[FIRDatabaseQuery sharedQueue] config:self.config];
+
+ // Needs to be called before authentication manager is instantiated
+ self.eventRaiser = [[FEventRaiser alloc] initWithQueue:self.config.callbackQueue];
+
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ [self deferredInit];
+ });
+ }
+ return self;
+}
+
+- (void)deferredInit {
+ // TODO: cleanup on dealloc
+ __weak FRepo *weakSelf = self;
+ [self.config.authTokenProvider listenForTokenChanges:^(NSString *token) {
+ [weakSelf.connection refreshAuthToken:token];
+ }];
+
+ // Open connection now so that by the time we are connected the deferred init has run
+ // This relies on the fact that all callbacks run on repos queue
+ self.connection.delegate = self;
+ [self.connection open];
+
+ self.dataUpdateCount = 0;
+ self.rangeMergeUpdateCount = 0;
+ self.interceptServerDataCallback = nil;
+
+ if (self.config.persistenceEnabled) {
+ NSString* repoHashString = [NSString stringWithFormat:@"%@_%@", self.repoInfo.host, self.repoInfo.namespace];
+ NSString* persistencePrefix = [NSString stringWithFormat:@"%@/%@", self.config.sessionIdentifier, repoHashString];
+
+ id<FCachePolicy> cachePolicy = [[FLRUCachePolicy alloc] initWithMaxSize:self.config.persistenceCacheSizeBytes];
+
+ id<FStorageEngine> engine;
+ if (self.config.forceStorageEngine != nil) {
+ engine = self.config.forceStorageEngine;
+ } else {
+ FLevelDBStorageEngine *levelDBEngine = [[FLevelDBStorageEngine alloc] initWithPath:persistencePrefix];
+ // We need the repo info to run the legacy migration. Future migrations will be managed by the database itself
+ // Remove this once we are confident that no-one is using legacy migration anymore...
+ [levelDBEngine runLegacyMigration:self.repoInfo];
+ engine = levelDBEngine;
+ }
+
+ self.persistenceManager = [[FPersistenceManager alloc] initWithStorageEngine:engine cachePolicy:cachePolicy];
+ } else {
+ self.persistenceManager = nil;
+ }
+
+ [self initTransactions];
+
+ // A list of data pieces and paths to be set when this client disconnects
+ self.onDisconnect = [[FSparseSnapshotTree alloc] init];
+ self.infoData = [[FSnapshotHolder alloc] init];
+
+ FListenProvider *infoListenProvider = [[FListenProvider alloc] init];
+ infoListenProvider.startListening = ^(FQuerySpec *query,
+ NSNumber *tagId,
+ id<FSyncTreeHash> hash,
+ fbt_nsarray_nsstring onComplete) {
+ NSArray *infoEvents = @[];
+ FRepo *strongSelf = weakSelf;
+ id<FNode> node = [strongSelf.infoData getNode:query.path];
+ // This is possibly a hack, but we have different semantics for .info endpoints. We don't raise null events
+ // on initial data...
+ if (![node isEmpty]) {
+ infoEvents = [strongSelf.infoSyncTree applyServerOverwriteAtPath:query.path newData:node];
+ [strongSelf.eventRaiser raiseCallback:^{
+ onComplete(kFWPResponseForActionStatusOk);
+ }];
+ }
+ return infoEvents;
+ };
+ infoListenProvider.stopListening = ^(FQuerySpec *query, NSNumber *tagId) {};
+ self.infoSyncTree = [[FSyncTree alloc] initWithListenProvider:infoListenProvider];
+
+ FListenProvider *serverListenProvider = [[FListenProvider alloc] init];
+ serverListenProvider.startListening = ^(FQuerySpec *query,
+ NSNumber *tagId,
+ id<FSyncTreeHash> hash,
+ fbt_nsarray_nsstring onComplete) {
+ [weakSelf.connection listen:query tagId:tagId hash:hash onComplete:^(NSString *status) {
+ NSArray *events = onComplete(status);
+ [weakSelf.eventRaiser raiseEvents:events];
+ }];
+ // No synchronous events for network-backed sync trees
+ return @[];
+ };
+ serverListenProvider.stopListening = ^(FQuerySpec *query, NSNumber *tag) {
+ [weakSelf.connection unlisten:query tagId:tag];
+ };
+ self.serverSyncTree = [[FSyncTree alloc] initWithPersistenceManager:self.persistenceManager
+ listenProvider:serverListenProvider];
+
+ [self restoreWrites];
+
+ [self updateInfo:kDotInfoConnected withValue:@NO];
+
+ [self setupNotifications];
+}
+
+
+- (void) restoreWrites {
+ NSArray *writes = self.persistenceManager.userWrites;
+
+ NSDictionary *serverValues = [FServerValues generateServerValues:self.serverClock];
+ __block NSInteger lastWriteId = NSIntegerMin;
+ [writes enumerateObjectsUsingBlock:^(FWriteRecord *write, NSUInteger idx, BOOL *stop) {
+ NSInteger writeId = write.writeId;
+ fbt_void_nsstring_nsstring callback = ^(NSString *status, NSString *errorReason) {
+ [self warnIfWriteFailedAtPath:write.path status:status message:@"Persisted write"];
+ [self ackWrite:writeId rerunTransactionsAtPath:write.path status:status];
+ };
+ if (lastWriteId >= writeId) {
+ [NSException raise:NSInternalInconsistencyException format:@"Restored writes were not in order!"];
+ }
+ lastWriteId = writeId;
+ self.writeIdCounter = writeId + 1;
+ if ([write isOverwrite]) {
+ FFLog(@"I-RDB038001", @"Restoring overwrite with id %ld", (long)write.writeId);
+ [self.connection putData:[write.overwrite valForExport:YES]
+ forPath:[write.path toString]
+ withHash:nil
+ withCallback:callback];
+ id<FNode> resolved = [FServerValues resolveDeferredValueSnapshot:write.overwrite withServerValues:serverValues];
+ [self.serverSyncTree applyUserOverwriteAtPath:write.path newData:resolved writeId:writeId isVisible:YES];
+ } else {
+ FFLog(@"I-RDB038002", @"Restoring merge with id %ld", (long)write.writeId);
+ [self.connection mergeData:[write.merge valForExport:YES]
+ forPath:[write.path toString]
+ withCallback:callback];
+ FCompoundWrite *resolved = [FServerValues resolveDeferredValueCompoundWrite:write.merge withServerValues:serverValues];
+ [self.serverSyncTree applyUserMergeAtPath:write.path changedChildren:resolved writeId:writeId];
+ }
+ }];
+}
+
+- (NSString*)name {
+ return self.repoInfo.namespace;
+}
+
+- (NSString *) description {
+ return [self.repoInfo description];
+}
+
+- (void) interrupt {
+ [self.connection interruptForReason:kFInterruptReasonRepoInterrupt];
+}
+
+- (void) resume {
+ [self.connection resumeForReason:kFInterruptReasonRepoInterrupt];
+}
+
+// NOTE: Typically if you're calling this, you should be in an @autoreleasepool block to make sure that ARC kicks
+// in and cleans up things no longer referenced (i.e. pendingPutsDB).
+- (void) dispose {
+ [self.connection interruptForReason:kFInterruptReasonRepoInterrupt];
+
+ // We need to nil out any references to LevelDB, to make sure the
+ // LevelDB exclusive locks are released.
+ [self.persistenceManager close];
+}
+
+- (NSInteger) nextWriteId {
+ return self->_writeIdCounter++;
+}
+
+- (NSTimeInterval) serverTime {
+ return [self.serverClock currentTime];
+}
+
+- (void) set:(FPath *)path withNode:(id<FNode>)node withCallback:(fbt_void_nserror_ref)onComplete {
+ id value = [node valForExport:YES];
+ FFLog(@"I-RDB038003", @"Setting: %@ with %@ pri: %@", [path toString], [value description], [[node getPriority] val]);
+
+ // TODO: Optimize this behavior to either (a) store flag to skip resolving where possible and / or
+ // (b) store unresolved paths on JSON parse
+ NSDictionary* serverValues = [FServerValues generateServerValues:self.serverClock];
+ id<FNode> newNode = [FServerValues resolveDeferredValueSnapshot:node withServerValues:serverValues];
+
+ NSInteger writeId = [self nextWriteId];
+ [self.persistenceManager saveUserOverwrite:node atPath:path writeId:writeId];
+ NSArray *events = [self.serverSyncTree applyUserOverwriteAtPath:path newData:newNode writeId:writeId isVisible:YES];
+ [self.eventRaiser raiseEvents:events];
+
+ [self.connection putData:value forPath:[path toString] withHash:nil withCallback:^(NSString *status, NSString *errorReason) {
+ [self warnIfWriteFailedAtPath:path status:status message:@"setValue: or removeValue:"];
+ [self ackWrite:writeId rerunTransactionsAtPath:path status:status];
+ [self callOnComplete:onComplete withStatus:status errorReason:errorReason andPath:path];
+ }];
+
+ FPath* affectedPath = [self abortTransactionsAtPath:path error:kFTransactionSet];
+ [self rerunTransactionsForPath:affectedPath];
+}
+
+- (void) update:(FPath *)path withNodes:(FCompoundWrite *)nodes withCallback:(fbt_void_nserror_ref)callback {
+ NSDictionary *values = [nodes valForExport:YES];
+
+ FFLog(@"I-RDB038004", @"Updating: %@ with %@", [path toString], [values description]);
+ NSDictionary* serverValues = [FServerValues generateServerValues:self.serverClock];
+ FCompoundWrite *resolved = [FServerValues resolveDeferredValueCompoundWrite:nodes withServerValues:serverValues];
+
+ if (!resolved.isEmpty) {
+ NSInteger writeId = [self nextWriteId];
+ [self.persistenceManager saveUserMerge:nodes atPath:path writeId:writeId];
+ NSArray *events = [self.serverSyncTree applyUserMergeAtPath:path changedChildren:resolved writeId:writeId];
+ [self.eventRaiser raiseEvents:events];
+
+ [self.connection mergeData:values forPath:[path description] withCallback:^(NSString *status, NSString *errorReason) {
+ [self warnIfWriteFailedAtPath:path status:status message:@"updateChildValues:"];
+ [self ackWrite:writeId rerunTransactionsAtPath:path status:status];
+ [self callOnComplete:callback withStatus:status errorReason:errorReason andPath:path];
+ }];
+
+ [nodes enumerateWrites:^(FPath *childPath, id<FNode> node, BOOL *stop) {
+ FPath* pathFromRoot = [path child:childPath];
+ FFLog(@"I-RDB038005", @"Cancelling transactions at path: %@", pathFromRoot);
+ FPath *affectedPath = [self abortTransactionsAtPath:pathFromRoot error:kFTransactionSet];
+ [self rerunTransactionsForPath:affectedPath];
+ }];
+ } else {
+ FFLog(@"I-RDB038006", @"update called with empty data. Doing nothing");
+ // Do nothing, just call the callback
+ [self callOnComplete:callback withStatus:@"ok" errorReason:nil andPath:path];
+ }
+}
+
+- (void) onDisconnectCancel:(FPath *)path withCallback:(fbt_void_nserror_ref)callback {
+ [self.connection onDisconnectCancelPath:path withCallback:^(NSString *status, NSString *errorReason) {
+ BOOL success = [status isEqualToString:kFWPResponseForActionStatusOk];
+ if (success) {
+ [self.onDisconnect forgetPath:path];
+ } else {
+ FFLog(@"I-RDB038007", @"cancelDisconnectOperations: at %@ failed: %@", path, status);
+ }
+
+ [self callOnComplete:callback withStatus:status errorReason:errorReason andPath:path];
+ }];
+}
+
+- (void) onDisconnectSet:(FPath *)path withNode:(id<FNode>)node withCallback:(fbt_void_nserror_ref)callback {
+ [self.connection onDisconnectPutData:[node valForExport:YES] forPath:path withCallback:^(NSString *status, NSString *errorReason) {
+ BOOL success = [status isEqualToString:kFWPResponseForActionStatusOk];
+ if (success) {
+ [self.onDisconnect rememberData:node onPath:path];
+ } else {
+ FFWarn(@"I-RDB038008", @"onDisconnectSetValue: or onDisconnectRemoveValue: at %@ failed: %@", path, status);
+ }
+
+ [self callOnComplete:callback withStatus:status errorReason:errorReason andPath:path];
+ }];
+}
+
+- (void) onDisconnectUpdate:(FPath *)path withNodes:(FCompoundWrite *)nodes withCallback:(fbt_void_nserror_ref)callback {
+ if (!nodes.isEmpty) {
+ NSDictionary *values = [nodes valForExport:YES];
+
+ [self.connection onDisconnectMergeData:values forPath:path withCallback:^(NSString *status, NSString *errorReason) {
+ BOOL success = [status isEqualToString:kFWPResponseForActionStatusOk];
+ if (success) {
+ [nodes enumerateWrites:^(FPath *relativePath, id<FNode> nodeUnresolved, BOOL *stop) {
+ FPath* childPath = [path child:relativePath];
+ [self.onDisconnect rememberData:nodeUnresolved onPath:childPath];
+ }];
+ } else {
+ FFWarn(@"I-RDB038009", @"onDisconnectUpdateChildValues: at %@ failed %@", path, status);
+ }
+
+ [self callOnComplete:callback withStatus:status errorReason:errorReason andPath:path];
+ }];
+ } else {
+ // Do nothing, just call the callback
+ [self callOnComplete:callback withStatus:@"ok" errorReason:nil andPath:path];
+ }
+}
+
+- (void) purgeOutstandingWrites {
+ FFLog(@"I-RDB038010", @"Purging outstanding writes");
+ NSArray *events = [self.serverSyncTree removeAllWrites];
+ [self.eventRaiser raiseEvents:events];
+ // Abort any transactions
+ [self abortTransactionsAtPath:[FPath empty] error:kFErrorWriteCanceled];
+ // Remove outstanding writes from connection
+ [self.connection purgeOutstandingWrites];
+}
+
+- (void) addEventRegistration:(id <FEventRegistration>)eventRegistration forQuery:(FQuerySpec *)query {
+ NSArray *events = nil;
+ if ([[query.path getFront] isEqualToString:kDotInfoPrefix]) {
+ events = [self.infoSyncTree addEventRegistration:eventRegistration forQuery:query];
+ } else {
+ events = [self.serverSyncTree addEventRegistration:eventRegistration forQuery:query];
+ }
+ [self.eventRaiser raiseEvents:events];
+}
+
+- (void) removeEventRegistration:(id<FEventRegistration>)eventRegistration forQuery:(FQuerySpec *)query {
+ // These are guaranteed not to raise events, since we're not passing in a cancelError. However we can future-proof
+ // a little bit by handling the return values anyways.
+ FFLog(@"I-RDB038011", @"Removing event registration with hande: %lu", (unsigned long)eventRegistration.handle);
+ NSArray *events = nil;
+ if ([[query.path getFront] isEqualToString:kDotInfoPrefix]) {
+ events = [self.infoSyncTree removeEventRegistration:eventRegistration forQuery:query cancelError:nil];
+ } else {
+ events = [self.serverSyncTree removeEventRegistration:eventRegistration forQuery:query cancelError:nil];
+ }
+ [self.eventRaiser raiseEvents:events];
+}
+
+- (void) keepQuery:(FQuerySpec *)query synced:(BOOL)synced {
+ NSAssert(![[query.path getFront] isEqualToString:kDotInfoPrefix], @"Can't keep .info tree synced!");
+ [self.serverSyncTree keepQuery:query synced:synced];
+}
+
+- (void) updateInfo:(NSString *) pathString withValue:(id)value {
+ // hack to make serverTimeOffset available in a threadsafe way. Property is marked as atomic
+ if ([pathString isEqualToString:kDotInfoServerTimeOffset]) {
+ NSTimeInterval offset = [(NSNumber *)value doubleValue]/1000.0;
+ self.serverClock = [[FOffsetClock alloc] initWithClock:[FSystemClock clock] offset:offset];
+ }
+
+ FPath* path = [[FPath alloc] initWith:[NSString stringWithFormat:@"%@/%@", kDotInfoPrefix, pathString]];
+ id<FNode> newNode = [FSnapshotUtilities nodeFrom:value];
+ [self.infoData updateSnapshot:path withNewSnapshot:newNode];
+ NSArray *events = [self.infoSyncTree applyServerOverwriteAtPath:path newData:newNode];
+ [self.eventRaiser raiseEvents:events];
+}
+
+- (void) callOnComplete:(fbt_void_nserror_ref)onComplete withStatus:(NSString *)status errorReason:(NSString *)errorReason andPath:(FPath *)path {
+ if (onComplete) {
+ FIRDatabaseReference * ref = [[FIRDatabaseReference alloc] initWithRepo:self path:path];
+ BOOL statusOk = [status isEqualToString:kFWPResponseForActionStatusOk];
+ NSError* err = nil;
+ if (!statusOk) {
+ err = [FUtilities errorForStatus:status andReason:errorReason];
+ }
+ [self.eventRaiser raiseCallback:^{
+ onComplete(err, ref);
+ }];
+ }
+}
+
+- (void)ackWrite:(NSInteger)writeId rerunTransactionsAtPath:(FPath *)path status:(NSString *)status {
+ if ([status isEqualToString:kFErrorWriteCanceled]) {
+ // This write was already removed, we just need to ignore it...
+ } else {
+ BOOL success = [status isEqualToString:kFWPResponseForActionStatusOk];
+ NSArray *clearEvents = [self.serverSyncTree ackUserWriteWithWriteId:writeId revert:!success persist:YES clock:self.serverClock];
+ if ([clearEvents count] > 0) {
+ [self rerunTransactionsForPath:path];
+ }
+ [self.eventRaiser raiseEvents:clearEvents];
+ }
+}
+
+- (void) warnIfWriteFailedAtPath:(FPath *)path status:(NSString *)status message:(NSString *)message {
+ if (!([status isEqualToString:kFWPResponseForActionStatusOk] || [status isEqualToString:kFErrorWriteCanceled])) {
+ FFWarn(@"I-RDB038012", @"%@ at %@ failed: %@", message, path, status);
+ }
+}
+
+#pragma mark -
+#pragma mark FPersistentConnectionDelegate methods
+
+- (void) onDataUpdate:(FPersistentConnection *)fpconnection forPath:(NSString *)pathString message:(id)data isMerge:(BOOL)isMerge tagId:(NSNumber *)tagId {
+ FFLog(@"I-RDB038013", @"onDataUpdateForPath: %@ withMessage: %@", pathString, data);
+
+ // For testing.
+ self.dataUpdateCount++;
+
+ FPath* path = [[FPath alloc] initWith:pathString];
+ data = self.interceptServerDataCallback ? self.interceptServerDataCallback(pathString, data) : data;
+ NSArray *events = nil;
+
+ if (tagId != nil) {
+ if (isMerge) {
+ NSDictionary *message = data;
+ FCompoundWrite *taggedChildren = [FCompoundWrite compoundWriteWithValueDictionary:message];
+ events = [self.serverSyncTree applyTaggedQueryMergeAtPath:path changedChildren:taggedChildren tagId:tagId];
+ } else {
+ id<FNode> taggedSnap = [FSnapshotUtilities nodeFrom:data];
+ events = [self.serverSyncTree applyTaggedQueryOverwriteAtPath:path newData:taggedSnap tagId:tagId];
+ }
+ } else if (isMerge) {
+ NSDictionary *message = data;
+ FCompoundWrite *changedChildren = [FCompoundWrite compoundWriteWithValueDictionary:message];
+ events = [self.serverSyncTree applyServerMergeAtPath:path changedChildren:changedChildren];
+ } else {
+ id<FNode> snap = [FSnapshotUtilities nodeFrom:data];
+ events = [self.serverSyncTree applyServerOverwriteAtPath:path newData:snap];
+ }
+
+ if ([events count] > 0) {
+ // Since we have a listener outstanding for each transaction, receiving any events
+ // is a proxy for some change having occurred.
+ [self rerunTransactionsForPath:path];
+ }
+
+ [self.eventRaiser raiseEvents:events];
+}
+
+- (void)onRangeMerge:(NSArray *)ranges forPath:(NSString *)pathString tagId:(NSNumber *)tag {
+ FFLog(@"I-RDB038014", @"onRangeMerge: %@ => %@", pathString, ranges);
+
+ // For testing
+ self.rangeMergeUpdateCount++;
+
+ FPath* path = [[FPath alloc] initWith:pathString];
+ NSArray *events;
+ if (tag != nil) {
+ events = [self.serverSyncTree applyTaggedServerRangeMergeAtPath:path updates:ranges tagId:tag];
+ } else {
+ events = [self.serverSyncTree applyServerRangeMergeAtPath:path updates:ranges];
+ }
+ if (events.count > 0) {
+ // Since we have a listener outstanding for each transaction, receiving any events
+ // is a proxy for some change having occurred.
+ [self rerunTransactionsForPath:path];
+ }
+
+ [self.eventRaiser raiseEvents:events];
+}
+
+- (void)onConnect:(FPersistentConnection *)fpconnection {
+ [self updateInfo:kDotInfoConnected withValue:@true];
+}
+
+- (void)onDisconnect:(FPersistentConnection *)fpconnection {
+ [self updateInfo:kDotInfoConnected withValue:@false];
+ [self runOnDisconnectEvents];
+}
+
+- (void)onServerInfoUpdate:(FPersistentConnection *)fpconnection updates:(NSDictionary *)updates {
+ for (NSString* key in updates) {
+ id val = [updates objectForKey:key];
+ [self updateInfo:key withValue:val];
+ }
+}
+
+- (void) setupNotifications {
+ NSString * const *backgroundConstant = (NSString * const *) dlsym(RTLD_DEFAULT, "UIApplicationDidEnterBackgroundNotification");
+ if (backgroundConstant) {
+ FFLog(@"I-RDB038015", @"Registering for background notification.");
+ [[NSNotificationCenter defaultCenter] addObserver:self
+ selector:@selector(didEnterBackground)
+ name:*backgroundConstant
+ object:nil];
+ } else {
+ FFLog(@"I-RDB038016", @"Skipped registering for background notification.");
+ }
+}
+
+- (void) didEnterBackground {
+ if (!self.config.persistenceEnabled)
+ return;
+
+ // Targetted compilation is ONLY for testing. UIKit is weak-linked in actual release build.
+#if TARGET_OS_IPHONE
+ // The idea is to wait until any outstanding sets get written to disk. Since the sets might still be in our
+ // dispatch queue, we wait for the dispatch queue to catch up and for persistence to catch up.
+ // This may be undesirable though. The dispatch queue might just be processing a bunch of incoming data or
+ // something. We might want to keep track of whether there are any unpersisted sets or something.
+ FFLog(@"I-RDB038017", @"Entering background. Starting background task to finish work.");
+ Class uiApplicationClass = NSClassFromString(@"UIApplication");
+ assert(uiApplicationClass); // If we are here, we should be on iOS and UIApplication should be available.
+
+ UIApplication *application = [uiApplicationClass sharedApplication];
+ __block UIBackgroundTaskIdentifier bgTask = [application beginBackgroundTaskWithExpirationHandler:^{
+ [application endBackgroundTask:bgTask];
+ }];
+
+ NSDate *start = [NSDate date];
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ NSTimeInterval finishTime = [start timeIntervalSinceNow]*-1;
+ FFLog(@"I-RDB038018", @"Background task completed. Queue time: %f", finishTime);
+ [application endBackgroundTask:bgTask];
+ });
+#endif
+}
+
+#pragma mark -
+#pragma mark Internal methods
+
+/**
+* Applies all the changes stored up in the onDisconnect tree
+*/
+- (void) runOnDisconnectEvents {
+ FFLog(@"I-RDB038019", @"Running onDisconnectEvents");
+ NSDictionary* serverValues = [FServerValues generateServerValues:self.serverClock];
+ FSparseSnapshotTree* resolvedTree = [FServerValues resolveDeferredValueTree:self.onDisconnect withServerValues:serverValues];
+ NSMutableArray *events = [[NSMutableArray alloc] init];
+
+ [resolvedTree forEachTreeAtPath:[FPath empty] do:^(FPath *path, id<FNode> node) {
+ [events addObjectsFromArray:[self.serverSyncTree applyServerOverwriteAtPath:path newData:node]];
+ FPath* affectedPath = [self abortTransactionsAtPath:path error:kFTransactionSet];
+ [self rerunTransactionsForPath:affectedPath];
+ }];
+
+ self.onDisconnect = [[FSparseSnapshotTree alloc] init];
+ [self.eventRaiser raiseEvents:events];
+}
+
+- (NSDictionary *) dumpListens {
+ return [self.connection dumpListens];
+}
+
+#pragma mark -
+#pragma mark Transactions
+
+/**
+ * Setup the transaction data structures
+ */
+- (void) initTransactions {
+ self.transactionQueueTree = [[FTree alloc] init];
+ self.hijackHash = NO;
+ self.loggedTransactionPersistenceWarning = NO;
+}
+
+/**
+ * Creates a new transaction, add its to the transactions we're tracking, and sends it to the server if possible
+ */
+- (void) startTransactionOnPath:(FPath *)path update:(fbt_transactionresult_mutabledata)update onComplete:(fbt_void_nserror_bool_datasnapshot)onComplete withLocalEvents:(BOOL)applyLocally {
+ if (self.config.persistenceEnabled && !self.loggedTransactionPersistenceWarning) {
+ self.loggedTransactionPersistenceWarning = YES;
+ FFInfo(@"I-RDB038020", @"runTransactionBlock: usage detected while persistence is enabled. Please be aware that transactions "
+ @"*will not* be persisted across app restarts. "
+ @"See https://www.firebase.com/docs/ios/guide/offline-capabilities.html#section-handling-transactions-offline for more details.");
+ }
+
+ FIRDatabaseReference * watchRef = [[FIRDatabaseReference alloc] initWithRepo:self path:path];
+ // make sure we're listening on this node
+ // Note: we can't do this asynchronously. To preserve event ordering, it has to be done in this block.
+ // This is ok, this block is guaranteed to be our own event loop
+ NSUInteger handle = [[FUtilities LUIDGenerator] integerValue];
+ fbt_void_datasnapshot cb = ^(FIRDataSnapshot *snapshot) {};
+ FValueEventRegistration *registration = [[FValueEventRegistration alloc] initWithRepo:self
+ handle:handle
+ callback:cb
+ cancelCallback:nil];
+ [watchRef.repo addEventRegistration:registration forQuery:watchRef.querySpec];
+ fbt_void_void unwatcher = ^{ [watchRef removeObserverWithHandle:handle]; };
+
+ // Save all the data that represents this transaction
+ FTupleTransaction* transaction = [[FTupleTransaction alloc] init];
+ transaction.path = path;
+ transaction.update = update;
+ transaction.onComplete = onComplete;
+ transaction.status = FTransactionInitializing;
+ transaction.order = [FUtilities LUIDGenerator];
+ transaction.applyLocally = applyLocally;
+ transaction.retryCount = 0;
+ transaction.unwatcher = unwatcher;
+ transaction.currentWriteId = nil;
+ transaction.currentInputSnapshot = nil;
+ transaction.currentOutputSnapshotRaw = nil;
+ transaction.currentOutputSnapshotResolved = nil;
+
+ // Run transaction initially
+ id<FNode> currentState = [self latestStateAtPath:path excludeWriteIds:nil];
+ transaction.currentInputSnapshot = currentState;
+ FIRMutableData * mutableCurrent = [[FIRMutableData alloc] initWithNode:currentState];
+ FIRTransactionResult * result = transaction.update(mutableCurrent);
+
+ if (!result.isSuccess) {
+ // Abort the transaction
+ transaction.unwatcher();
+ transaction.currentOutputSnapshotRaw = nil;
+ transaction.currentOutputSnapshotResolved = nil;
+ if (transaction.onComplete) {
+ FIRDatabaseReference *ref = [[FIRDatabaseReference alloc] initWithRepo:self path:transaction.path];
+ FIndexedNode *indexedNode = [FIndexedNode indexedNodeWithNode:transaction.currentInputSnapshot];
+ FIRDataSnapshot *snap = [[FIRDataSnapshot alloc] initWithRef:ref indexedNode:indexedNode];
+ [self.eventRaiser raiseCallback:^{
+ transaction.onComplete(nil, NO, snap);
+ }];
+ }
+ } else {
+ // Note: different from js. We don't need to validate, FIRMutableData does validation.
+ // We also don't have to worry about priorities. Just mark as run and add to queue.
+ transaction.status = FTransactionRun;
+ FTree* queueNode = [self.transactionQueueTree subTree:transaction.path];
+ NSMutableArray* nodeQueue = [queueNode getValue];
+ if (nodeQueue == nil) {
+ nodeQueue = [[NSMutableArray alloc] init];
+ }
+ [nodeQueue addObject:transaction];
+ [queueNode setValue:nodeQueue];
+
+ // Update visibleData and raise events
+ // Note: We intentionally raise events after updating all of our transaction state, since the user could
+ // start new transactions from the event callbacks
+ NSDictionary* serverValues = [FServerValues generateServerValues:self.serverClock];
+ id<FNode> newValUnresolved = [result.update nodeValue];
+ id<FNode> newVal = [FServerValues resolveDeferredValueSnapshot:newValUnresolved withServerValues:serverValues];
+ transaction.currentOutputSnapshotRaw = newValUnresolved;
+ transaction.currentOutputSnapshotResolved = newVal;
+ transaction.currentWriteId = [NSNumber numberWithInteger:[self nextWriteId]];
+
+ NSArray *events = [self.serverSyncTree applyUserOverwriteAtPath:path newData:newVal
+ writeId:[transaction.currentWriteId integerValue]
+ isVisible:transaction.applyLocally];
+ [self.eventRaiser raiseEvents:events];
+
+ [self sendAllReadyTransactions];
+ }
+}
+
+/**
+ * @param writeIdsToExclude A specific set to exclude
+ */
+- (id<FNode>) latestStateAtPath:(FPath *)path excludeWriteIds:(NSArray *)writeIdsToExclude {
+ id<FNode> latestState = [self.serverSyncTree calcCompleteEventCacheAtPath:path excludeWriteIds:writeIdsToExclude];
+ return latestState ? latestState : [FEmptyNode emptyNode];
+}
+
+/**
+ * Sends any already-run transactions that aren't waiting for outstanding transactions to complete.
+ *
+ * Externally, call the version with no arguments.
+ * Internally, calls itself recursively with a particular transactionQueueTree node to recurse through the tree
+ */
+- (void) sendAllReadyTransactions {
+ FTree* node = self.transactionQueueTree;
+
+ [self pruneCompletedTransactionsBelowNode:node];
+ [self sendReadyTransactionsForTree:node];
+}
+
+- (void) sendReadyTransactionsForTree:(FTree *)node {
+ NSMutableArray* queue = [node getValue];
+ if (queue != nil) {
+ queue = [self buildTransactionQueueAtNode:node];
+ NSAssert([queue count] > 0, @"Sending zero length transaction queue");
+
+ NSUInteger notRunIndex = [queue indexOfObjectPassingTest:^BOOL(id obj, NSUInteger idx, BOOL *stop) {
+ return ((FTupleTransaction*)obj).status != FTransactionRun;
+ }];
+
+ // If they're all run (and not sent), we can send them. Else, we must wait.
+ if (notRunIndex == NSNotFound) {
+ [self sendTransactionQueue:queue atPath:node.path];
+ }
+ } else if ([node hasChildren]) {
+ [node forEachChild:^(FTree *child) {
+ [self sendReadyTransactionsForTree:child];
+ }];
+ }
+}
+
+/**
+ * Given a list of run transactions, send them to the server and then handle the result (success or failure).
+ */
+- (void) sendTransactionQueue:(NSMutableArray *)queue atPath:(FPath *)path {
+ // Mark transactions as sent and bump the retry count
+ NSMutableArray *writeIdsToExclude = [[NSMutableArray alloc] init];
+ for (FTupleTransaction *transaction in queue) {
+ [writeIdsToExclude addObject:transaction.currentWriteId];
+ }
+ id<FNode> latestState = [self latestStateAtPath:path excludeWriteIds:writeIdsToExclude];
+ id<FNode> snapToSend = latestState;
+ NSString *latestHash = [latestState dataHash];
+ for (FTupleTransaction* transaction in queue) {
+ NSAssert(transaction.status == FTransactionRun, @"[FRepo sendTransactionQueue:] items in queue should all be run.");
+ FFLog(@"I-RDB038021", @"Transaction at %@ set to SENT", transaction.path);
+ transaction.status = FTransactionSent;
+ transaction.retryCount++;
+ FPath *relativePath = [FPath relativePathFrom:path to:transaction.path];
+ // If we've gotten to this point, the output snapshot must be defined.
+ snapToSend = [snapToSend updateChild:relativePath withNewChild:transaction.currentOutputSnapshotRaw];
+ }
+
+ id dataToSend = [snapToSend valForExport:YES];
+ NSString *pathToSend = [path description];
+ latestHash = self.hijackHash ? @"badhash" : latestHash;
+
+ // Send the put
+ [self.connection putData:dataToSend forPath:pathToSend withHash:latestHash withCallback:^(NSString *status, NSString *errorReason) {
+ FFLog(@"I-RDB038022", @"Transaction put response: %@ : %@", pathToSend, status);
+
+ NSMutableArray *events = [[NSMutableArray alloc] init];
+ if ([status isEqualToString:kFWPResponseForActionStatusOk]) {
+ // Queue up the callbacks and fire them after cleaning up all of our transaction state, since
+ // the callback could trigger more transactions or sets.
+ NSMutableArray *callbacks = [[NSMutableArray alloc] init];
+ for (FTupleTransaction *transaction in queue) {
+ transaction.status = FTransactionCompleted;
+ [events addObjectsFromArray:[self.serverSyncTree ackUserWriteWithWriteId:[transaction.currentWriteId integerValue]
+ revert:NO
+ persist:NO
+ clock:self.serverClock]];
+ if (transaction.onComplete) {
+ // We never unset the output snapshot, and given that this transaction is complete, it should be set
+ id <FNode> node = transaction.currentOutputSnapshotResolved;
+ FIndexedNode *indexedNode = [FIndexedNode indexedNodeWithNode:node];
+ FIRDatabaseReference *ref = [[FIRDatabaseReference alloc] initWithRepo:self path:transaction.path];
+ FIRDataSnapshot *snapshot = [[FIRDataSnapshot alloc] initWithRef:ref indexedNode:indexedNode];
+ fbt_void_void cb = ^{
+ transaction.onComplete(nil, YES, snapshot);
+ };
+ [callbacks addObject:[cb copy]];
+ }
+ transaction.unwatcher();
+ }
+
+ // Now remove the completed transactions.
+ [self pruneCompletedTransactionsBelowNode:[self.transactionQueueTree subTree:path]];
+ // There may be pending transactions that we can now send.
+ [self sendAllReadyTransactions];
+
+ // Finally, trigger onComplete callbacks
+ [self.eventRaiser raiseCallbacks:callbacks];
+ } else {
+ // transactions are no longer sent. Update their status appropriately.
+ if ([status isEqualToString:kFWPResponseForActionStatusDataStale]) {
+ for (FTupleTransaction *transaction in queue) {
+ if (transaction.status == FTransactionSentNeedsAbort) {
+ transaction.status = FTransactionNeedsAbort;
+ } else {
+ transaction.status = FTransactionRun;
+ }
+ }
+ } else {
+ FFWarn(@"I-RDB038023", @"runTransactionBlock: at %@ failed: %@", path, status);
+ for (FTupleTransaction *transaction in queue) {
+ transaction.status = FTransactionNeedsAbort;
+ [transaction setAbortStatus:status reason:errorReason];
+ }
+ }
+ }
+
+ [self rerunTransactionsForPath:path];
+ [self.eventRaiser raiseEvents:events];
+ }];
+}
+
+/**
+ * Finds all transactions dependent on the data at changed Path and reruns them.
+ *
+ * Should be called any time cached data changes.
+ *
+ * Return the highest path that was affected by rerunning transactions. This is the path at which events need to
+ * be raised for.
+ */
+- (FPath *) rerunTransactionsForPath:(FPath *)changedPath {
+ // For the common case that there are no transactions going on, skip all this!
+ if ([self.transactionQueueTree isEmpty]) {
+ return changedPath;
+ } else {
+ FTree* rootMostTransactionNode = [self getAncestorTransactionNodeForPath:changedPath];
+ FPath* path = rootMostTransactionNode.path;
+
+ NSArray* queue = [self buildTransactionQueueAtNode:rootMostTransactionNode];
+ [self rerunTransactionQueue:queue atPath:path];
+
+ return path;
+ }
+}
+
+/**
+ * Does all the work of rerunning transactions (as well as cleans up aborted transactions and whatnot).
+ */
+- (void) rerunTransactionQueue:(NSArray *)queue atPath:(FPath *)path {
+ if (queue.count == 0) {
+ return; // nothing to do
+ }
+
+ // Queue up the callbacks and fire them after cleaning up all of our transaction state, since
+ // the callback could trigger more transactions or sets.
+ NSMutableArray *events = [[NSMutableArray alloc] init];
+ NSMutableArray *callbacks = [[NSMutableArray alloc] init];
+
+ // Ignore, by default, all of the sets in this queue, since we're re-running all of them. However, we want to include
+ // the results of new sets triggered as part of this re-run, so we don't want to ignore a range, just these specific
+ // sets.
+ NSMutableArray *writeIdsToExclude = [[NSMutableArray alloc] init];
+ for (FTupleTransaction *transaction in queue) {
+ [writeIdsToExclude addObject:transaction.currentWriteId];
+ }
+
+ for (FTupleTransaction* transaction in queue) {
+ FPath* relativePath __unused = [FPath relativePathFrom:path to:transaction.path];
+ BOOL abortTransaction = NO;
+ NSAssert(relativePath != nil, @"[FRepo rerunTransactionsQueue:] relativePath should not be null.");
+
+ if (transaction.status == FTransactionNeedsAbort) {
+ abortTransaction = YES;
+ if (![transaction.abortStatus isEqualToString:kFErrorWriteCanceled]) {
+ NSArray *ackEvents = [self.serverSyncTree ackUserWriteWithWriteId:[transaction.currentWriteId integerValue]
+ revert:YES
+ persist:NO
+ clock:self.serverClock];
+ [events addObjectsFromArray:ackEvents];
+ }
+ } else if (transaction.status == FTransactionRun) {
+ if (transaction.retryCount >= kFTransactionMaxRetries) {
+ abortTransaction = YES;
+ [transaction setAbortStatus:kFTransactionTooManyRetries reason:nil];
+ [events addObjectsFromArray:[self.serverSyncTree ackUserWriteWithWriteId:[transaction.currentWriteId integerValue]
+ revert:YES
+ persist:NO
+ clock:self.serverClock]];
+ } else {
+ // This code reruns a transaction
+ id<FNode> currentNode = [self latestStateAtPath:transaction.path excludeWriteIds:writeIdsToExclude];
+ transaction.currentInputSnapshot = currentNode;
+ FIRMutableData * mutableCurrent = [[FIRMutableData alloc] initWithNode:currentNode];
+ FIRTransactionResult * result = transaction.update(mutableCurrent);
+ if (result.isSuccess) {
+ NSNumber *oldWriteId = transaction.currentWriteId;
+ NSDictionary* serverValues = [FServerValues generateServerValues:self.serverClock];
+
+ id<FNode> newVal = [result.update nodeValue];
+ id<FNode> newValResolved = [FServerValues resolveDeferredValueSnapshot:newVal withServerValues:serverValues];
+
+ transaction.currentOutputSnapshotRaw = newVal;
+ transaction.currentOutputSnapshotResolved = newValResolved;
+
+ transaction.currentWriteId = [NSNumber numberWithInteger:[self nextWriteId]];
+ // Mutates writeIdsToExclude in place
+ [writeIdsToExclude removeObject:oldWriteId];
+ [events addObjectsFromArray:[self.serverSyncTree applyUserOverwriteAtPath:transaction.path
+ newData:transaction.currentOutputSnapshotResolved
+ writeId:[transaction.currentWriteId integerValue]
+ isVisible:transaction.applyLocally]];
+ [events addObjectsFromArray:[self.serverSyncTree ackUserWriteWithWriteId:[oldWriteId integerValue]
+ revert:YES
+ persist:NO
+ clock:self.serverClock]];
+ } else {
+ abortTransaction = YES;
+ // The user aborted the transaction. JS treats ths as a "nodata" abort, but it's not an error, so we don't send them an error.
+ [transaction setAbortStatus:nil reason:nil];
+ [events addObjectsFromArray:[self.serverSyncTree ackUserWriteWithWriteId:[transaction.currentWriteId integerValue]
+ revert:YES
+ persist:NO
+ clock:self.serverClock]];
+ }
+ }
+ }
+
+ [self.eventRaiser raiseEvents:events];
+ events = nil;
+
+ if (abortTransaction) {
+ // Abort
+ transaction.status = FTransactionCompleted;
+ transaction.unwatcher();
+ if (transaction.onComplete) {
+ FIRDatabaseReference * ref = [[FIRDatabaseReference alloc] initWithRepo:self path:transaction.path];
+ FIndexedNode *lastInput = [FIndexedNode indexedNodeWithNode:transaction.currentInputSnapshot];
+ FIRDataSnapshot * snap = [[FIRDataSnapshot alloc] initWithRef:ref indexedNode:lastInput];
+ fbt_void_void cb = ^{
+ // Unlike JS, no need to check for "nodata" because ObjC has abortError = nil
+ transaction.onComplete(transaction.abortError, NO, snap);
+ };
+ [callbacks addObject:[cb copy]];
+ }
+ }
+ }
+
+ // Note: unlike current js client, we don't need to preserve priority. Users can set priority via FIRMutableData
+
+ // Clean up completed transactions.
+ [self pruneCompletedTransactionsBelowNode:self.transactionQueueTree];
+
+ // Now fire callbacks, now that we're in a good, known state.
+ [self.eventRaiser raiseCallbacks:callbacks];
+
+ // Try to send the transaction result to the server
+ [self sendAllReadyTransactions];
+}
+
+- (FTree *) getAncestorTransactionNodeForPath:(FPath *)path {
+ FTree* transactionNode = self.transactionQueueTree;
+
+ while (![path isEmpty] && [transactionNode getValue] == nil) {
+ NSString* front = [path getFront];
+ transactionNode = [transactionNode subTree:[[FPath alloc] initWith:front]];
+ path = [path popFront];
+ }
+
+ return transactionNode;
+}
+
+- (NSMutableArray *) buildTransactionQueueAtNode:(FTree *)node {
+ NSMutableArray* queue = [[NSMutableArray alloc] init];
+ [self aggregateTransactionQueuesForNode:node andQueue:queue];
+
+ [queue sortUsingComparator:^NSComparisonResult(FTupleTransaction* obj1, FTupleTransaction* obj2) {
+ return [obj1.order compare:obj2.order];
+ }];
+
+ return queue;
+}
+
+- (void) aggregateTransactionQueuesForNode:(FTree *)node andQueue:(NSMutableArray *)queue {
+ NSArray* nodeQueue = [node getValue];
+ [queue addObjectsFromArray:nodeQueue];
+
+ [node forEachChild:^(FTree *child) {
+ [self aggregateTransactionQueuesForNode:child andQueue:queue];
+ }];
+}
+
+/**
+ * Remove COMPLETED transactions at or below this node in the transactionQueueTree
+ */
+- (void) pruneCompletedTransactionsBelowNode:(FTree *)node {
+ NSMutableArray* queue = [node getValue];
+ if (queue != nil) {
+ int i = 0;
+ // remove all of the completed transactions from the queue
+ while (i < queue.count) {
+ FTupleTransaction* transaction = [queue objectAtIndex:i];
+ if (transaction.status == FTransactionCompleted) {
+ [queue removeObjectAtIndex:i];
+ } else {
+ i++;
+ }
+ }
+ if (queue.count > 0) {
+ [node setValue:queue];
+ } else {
+ [node setValue:nil];
+ }
+ }
+
+ [node forEachChildMutationSafe:^(FTree *child) {
+ [self pruneCompletedTransactionsBelowNode:child];
+ }];
+}
+
+/**
+ * Aborts all transactions on ancestors or descendants of the specified path. Called when doing a setValue: or
+ * updateChildValues: since we consider them incompatible with transactions
+ *
+ * @param path path for which we want to abort related transactions.
+ */
+- (FPath *) abortTransactionsAtPath:(FPath *)path error:(NSString *)error {
+ // For the common case that there are no transactions going on, skip all this!
+ if ([self.transactionQueueTree isEmpty]) {
+ return path;
+ } else {
+ FPath* affectedPath = [self getAncestorTransactionNodeForPath:path].path;
+
+ FTree* transactionNode = [self.transactionQueueTree subTree:path];
+ [transactionNode forEachAncestor:^BOOL(FTree *ancestor) {
+ [self abortTransactionsAtNode:ancestor error:error];
+ return NO;
+ }];
+
+ [self abortTransactionsAtNode:transactionNode error:error];
+
+ [transactionNode forEachDescendant:^(FTree *child) {
+ [self abortTransactionsAtNode:child error:error];
+ }];
+
+ return affectedPath;
+ }
+}
+
+/**
+ * Abort transactions stored in this transactions queue node.
+ *
+ * @param node Node to abort transactions for.
+ */
+- (void) abortTransactionsAtNode:(FTree *)node error:(NSString *)error {
+ NSMutableArray* queue = [node getValue];
+ if (queue != nil) {
+
+ // Queue up the callbacks and fire them after cleaning up all of our transaction state, since
+ // can be immediately aborted and removed.
+ NSMutableArray* callbacks = [[NSMutableArray alloc] init];
+
+ // Go through queue. Any already-sent transactions must be marked for abort, while the unsent ones
+ // can be immediately aborted and removed
+ NSMutableArray *events = [[NSMutableArray alloc] init];
+ int lastSent = -1;
+ // Note: all of the sent transactions will be at the front of the queue, so safe to increment lastSent
+ for (FTupleTransaction* transaction in queue) {
+ if (transaction.status == FTransactionSentNeedsAbort) {
+ // No-op. already marked.
+ } else if (transaction.status == FTransactionSent) {
+ // Mark this transaction for abort when it returns
+ lastSent++;
+ transaction.status = FTransactionSentNeedsAbort;
+ [transaction setAbortStatus:error reason:nil];
+ } else {
+ // we can abort this immediately
+ transaction.unwatcher();
+ if ([error isEqualToString:kFTransactionSet]) {
+ [events addObjectsFromArray:[self.serverSyncTree ackUserWriteWithWriteId:[transaction.currentWriteId integerValue]
+ revert:YES
+ persist:NO
+ clock:self.serverClock]];
+ } else {
+ // If it was cancelled it was already removed from the sync tree, no need to ack
+ NSAssert([error isEqualToString:kFErrorWriteCanceled], nil);
+ }
+
+ if (transaction.onComplete) {
+ NSError* abortReason = [FUtilities errorForStatus:error andReason:nil];
+ FIRDataSnapshot * snapshot = nil;
+ fbt_void_void cb = ^{
+ transaction.onComplete(abortReason, NO, snapshot);
+ };
+ [callbacks addObject:[cb copy]];
+ }
+ }
+ }
+ if (lastSent == -1) {
+ // We're not waiting for any sent transactions. We can clear the queue.
+ [node setValue:nil];
+ } else {
+ // Remove the transactions we aborted
+ NSRange theRange;
+ theRange.location = lastSent + 1;
+ theRange.length = queue.count - theRange.location;
+ [queue removeObjectsInRange:theRange];
+ }
+
+ // Now fire the callbacks
+ [self.eventRaiser raiseEvents:events];
+ [self.eventRaiser raiseCallbacks:callbacks];
+ }
+}
+
+@end
diff --git a/Firebase/Database/Core/FRepoInfo.h b/Firebase/Database/Core/FRepoInfo.h
new file mode 100644
index 0000000..dace937
--- /dev/null
+++ b/Firebase/Database/Core/FRepoInfo.h
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+@interface FRepoInfo : NSObject
+
+@property (nonatomic, readonly, strong) NSString* host;
+@property (nonatomic, readonly, strong) NSString* namespace;
+@property (nonatomic, strong) NSString* internalHost;
+@property (nonatomic, readonly) bool secure;
+
+- (id) initWithHost:(NSString*)host isSecure:(bool)secure withNamespace:(NSString*)namespace;
+
+- (NSString *) connectionURLWithLastSessionID:(NSString*)lastSessionID;
+- (NSString *) connectionURL;
+- (void) clearInternalHostCache;
+- (BOOL) isDemoHost;
+- (BOOL) isCustomHost;
+
+@end
diff --git a/Firebase/Database/Core/FRepoInfo.m b/Firebase/Database/Core/FRepoInfo.m
new file mode 100644
index 0000000..6b15fe5
--- /dev/null
+++ b/Firebase/Database/Core/FRepoInfo.m
@@ -0,0 +1,115 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FRepoInfo.h"
+#import "FConstants.h"
+
+@interface FRepoInfo ()
+
+@property (nonatomic, strong) NSString *domain;
+
+@end
+
+
+@implementation FRepoInfo
+
+@synthesize namespace;
+@synthesize host;
+@synthesize internalHost;
+@synthesize secure;
+@synthesize domain;
+
+- (id) initWithHost:(NSString*)aHost isSecure:(bool)isSecure withNamespace:(NSString*)aNamespace {
+ self = [super init];
+ if (self) {
+ host = aHost;
+ domain = [host substringFromIndex:[host rangeOfString:@"."].location+1];
+ secure = isSecure;
+ namespace = aNamespace;
+
+ // Get cached internal host if it exists
+ NSString* internalHostKey = [NSString stringWithFormat:@"firebase:host:%@", self.host];
+ NSString* cachedInternalHost = [[NSUserDefaults standardUserDefaults] stringForKey:internalHostKey];
+ if (cachedInternalHost != nil) {
+ internalHost = cachedInternalHost;
+ } else {
+ internalHost = self.host;
+ }
+ }
+ return self;
+}
+
+- (NSString *)description {
+ // The namespace is encoded in the hostname, so we can just return this.
+ return [NSString stringWithFormat:@"http%@://%@", (self.secure ? @"s" : @""), self.host];
+}
+
+- (void) setInternalHost:(NSString *)newHost {
+ if (![internalHost isEqualToString:newHost]) {
+ internalHost = newHost;
+
+ // Cache the internal host so we don't need to redirect later on
+ NSString* internalHostKey = [NSString stringWithFormat:@"firebase:host:%@", self.host];
+ NSUserDefaults* cache = [NSUserDefaults standardUserDefaults];
+ [cache setObject:internalHost forKey:internalHostKey];
+ [cache synchronize];
+ }
+}
+
+- (void) clearInternalHostCache {
+ internalHost = self.host;
+
+ // Remove the cached entry
+ NSString* internalHostKey = [NSString stringWithFormat:@"firebase:host:%@", self.host];
+ NSUserDefaults* cache = [NSUserDefaults standardUserDefaults];
+ [cache removeObjectForKey:internalHostKey];
+ [cache synchronize];
+}
+
+- (BOOL) isDemoHost {
+ return [self.domain isEqualToString:@"firebaseio-demo.com"];
+}
+
+- (BOOL) isCustomHost {
+ return ![self.domain isEqualToString:@"firebaseio-demo.com"] && ![self.domain isEqualToString:@"firebaseio.com"];
+}
+
+
+- (NSString *) connectionURL {
+ return [self connectionURLWithLastSessionID:nil];
+}
+
+- (NSString *) connectionURLWithLastSessionID:(NSString*)lastSessionID {
+ NSString *scheme;
+ if (self.secure) {
+ scheme = @"wss";
+ } else {
+ scheme = @"ws";
+ }
+ NSString *url = [NSString stringWithFormat:@"%@://%@/.ws?%@=%@&ns=%@",
+ scheme,
+ self.internalHost,
+ kWireProtocolVersionParam,
+ kWebsocketProtocolVersion,
+ self.namespace];
+
+ if (lastSessionID != nil) {
+ url = [NSString stringWithFormat:@"%@&ls=%@", url, lastSessionID];
+ }
+ return url;
+}
+
+@end
diff --git a/Firebase/Database/Core/FRepoManager.h b/Firebase/Database/Core/FRepoManager.h
new file mode 100644
index 0000000..c492861
--- /dev/null
+++ b/Firebase/Database/Core/FRepoManager.h
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+#import "FRepoInfo.h"
+#import "FRepo.h"
+#import "FIRDatabaseConfig.h"
+
+@interface FRepoManager : NSObject
+
++ (FRepo *) getRepo:(FRepoInfo *)repoInfo config:(FIRDatabaseConfig *)config;
++ (FRepo *) createRepo:(FRepoInfo *)repoInfo config:(FIRDatabaseConfig *)config database:(FIRDatabase *)database;
++ (void) interruptAll;
++ (void) interrupt:(FIRDatabaseConfig *)config;
++ (void) resumeAll;
++ (void) resume:(FIRDatabaseConfig *)config;
++ (void) disposeRepos:(FIRDatabaseConfig *)config;
+
+@end
diff --git a/Firebase/Database/Core/FRepoManager.m b/Firebase/Database/Core/FRepoManager.m
new file mode 100644
index 0000000..6dccf7e
--- /dev/null
+++ b/Firebase/Database/Core/FRepoManager.m
@@ -0,0 +1,131 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FRepoManager.h"
+#import "FRepo.h"
+#import "FIRDatabaseQuery_Private.h"
+#import "FAtomicNumber.h"
+#import "FIRDatabaseConfig_Private.h"
+#import "FIRDatabase_Private.h"
+
+@implementation FRepoManager
+
++ (NSMutableDictionary *)configs {
+ static dispatch_once_t pred = 0;
+ static NSMutableDictionary *configs;
+ dispatch_once(&pred, ^{
+ configs = [NSMutableDictionary dictionary];
+ });
+ return configs;
+}
+
+/**
+ * Used for legacy unit tests. The public API should go through FirebaseDatabase which
+ * calls createRepo.
+ */
++ (FRepo *) getRepo:(FRepoInfo *)repoInfo config:(FIRDatabaseConfig *)config {
+ [config freeze];
+ NSString* repoHashString = [NSString stringWithFormat:@"%@_%@", repoInfo.host, repoInfo.namespace];
+ NSMutableDictionary *configs = [FRepoManager configs];
+ @synchronized(configs) {
+ NSMutableDictionary *repos = configs[config.sessionIdentifier];
+ if (!repos || repos[repoHashString] == nil) {
+ // Calling this should create the repo.
+ [FIRDatabase createDatabaseForTests:repoInfo config:config];
+ }
+
+ return configs[config.sessionIdentifier][repoHashString];
+ }
+}
+
++ (FRepo *) createRepo:(FRepoInfo *)repoInfo config:(FIRDatabaseConfig *)config database:(FIRDatabase *)database {
+ [config freeze];
+ NSString* repoHashString = [NSString stringWithFormat:@"%@_%@", repoInfo.host, repoInfo.namespace];
+ NSMutableDictionary *configs = [FRepoManager configs];
+ @synchronized(configs) {
+ NSMutableDictionary *repos = configs[config.sessionIdentifier];
+ if (!repos) {
+ repos = [NSMutableDictionary dictionary];
+ configs[config.sessionIdentifier] = repos;
+ }
+ FRepo *repo = repos[repoHashString];
+ if (repo == nil) {
+ repo = [[FRepo alloc] initWithRepoInfo:repoInfo config:config database:database];
+ repos[repoHashString] = repo;
+ return repo;
+ } else {
+ [NSException raise:@"RepoExists" format:@"createRepo called for Repo that already exists."];
+ return nil;
+ }
+ }
+}
+
++ (void) interrupt:(FIRDatabaseConfig *)config {
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ NSMutableDictionary *configs = [FRepoManager configs];
+ NSMutableDictionary *repos = configs[config.sessionIdentifier];
+ for (FRepo* repo in [repos allValues]) {
+ [repo interrupt];
+ }
+ });
+}
+
++ (void) interruptAll {
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ NSMutableDictionary *configs = [FRepoManager configs];
+ for (NSMutableDictionary *repos in [configs allValues]) {
+ for (FRepo* repo in [repos allValues]) {
+ [repo interrupt];
+ }
+ }
+ });
+}
+
++ (void) resume:(FIRDatabaseConfig *)config {
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ NSMutableDictionary *configs = [FRepoManager configs];
+ NSMutableDictionary *repos = configs[config.sessionIdentifier];
+ for (FRepo* repo in [repos allValues]) {
+ [repo resume];
+ }
+ });
+}
+
++ (void) resumeAll {
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ NSMutableDictionary *configs = [FRepoManager configs];
+ for (NSMutableDictionary *repos in [configs allValues]) {
+ for (FRepo* repo in [repos allValues]) {
+ [repo resume];
+ }
+ }
+ });
+}
+
++ (void)disposeRepos:(FIRDatabaseConfig *)config {
+ // Do this synchronously to make sure we release our references to LevelDB before returning, allowing LevelDB
+ // to close and release its exclusive locks.
+ dispatch_sync([FIRDatabaseQuery sharedQueue], ^{
+ FFLog(@"I-RDB040001", @"Disposing all repos for Config with name %@", config.sessionIdentifier);
+ NSMutableDictionary *configs = [FRepoManager configs];
+ for (FRepo* repo in [configs[config.sessionIdentifier] allValues]) {
+ [repo dispose];
+ }
+ [configs removeObjectForKey:config.sessionIdentifier];
+ });
+}
+
+@end
diff --git a/Firebase/Database/Core/FRepo_Private.h b/Firebase/Database/Core/FRepo_Private.h
new file mode 100644
index 0000000..109edac
--- /dev/null
+++ b/Firebase/Database/Core/FRepo_Private.h
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FRepo.h"
+#import "FSparseSnapshotTree.h"
+
+@class FSyncTree;
+@class FAtomicNumber;
+@class FEventRaiser;
+@class FSnapshotHolder;
+
+@interface FRepo ()
+
+- (void) runOnDisconnectEvents;
+
+@property (nonatomic, strong) FRepoInfo* repoInfo;
+@property (nonatomic, strong) FPersistentConnection* connection;
+@property (nonatomic, strong) FSnapshotHolder* infoData;
+@property (nonatomic, strong) FSparseSnapshotTree* onDisconnect;
+@property (nonatomic, strong) FEventRaiser *eventRaiser;
+@property (nonatomic, strong) FSyncTree *serverSyncTree;
+
+// For testing.
+@property (nonatomic) long dataUpdateCount;
+@property (nonatomic) long rangeMergeUpdateCount;
+
+- (NSInteger)nextWriteId;
+
+@end
diff --git a/Firebase/Database/Core/FServerValues.h b/Firebase/Database/Core/FServerValues.h
new file mode 100644
index 0000000..2540c12
--- /dev/null
+++ b/Firebase/Database/Core/FServerValues.h
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+#import "FSparseSnapshotTree.h"
+#import "FNode.h"
+#import "FCompoundWrite.h"
+#import "FClock.h"
+
+@interface FServerValues : NSObject
+
++ (NSDictionary*) generateServerValues:(id<FClock>)clock;
++ (id) resolveDeferredValueCompoundWrite:(FCompoundWrite*)write withServerValues:(NSDictionary*)serverValues;
++ (id<FNode>) resolveDeferredValueSnapshot:(id<FNode>)node withServerValues:(NSDictionary*)serverValues;
++ (id) resolveDeferredValueTree:(FSparseSnapshotTree*)tree withServerValues:(NSDictionary*)serverValues;
+
+@end
diff --git a/Firebase/Database/Core/FServerValues.m b/Firebase/Database/Core/FServerValues.m
new file mode 100644
index 0000000..89ee5d0
--- /dev/null
+++ b/Firebase/Database/Core/FServerValues.m
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FServerValues.h"
+#import "FConstants.h"
+#import "FLeafNode.h"
+#import "FChildrenNode.h"
+#import "FSnapshotUtilities.h"
+
+@implementation FServerValues
+
++ (NSDictionary*) generateServerValues:(id<FClock>)clock {
+ long long millis = (long long)([clock currentTime] * 1000);
+ return @{ @"timestamp": [NSNumber numberWithLongLong:millis] };
+}
+
++ (id) resolveDeferredValue:(id)val withServerValues:(NSDictionary*)serverValues {
+ if ([val isKindOfClass:[NSDictionary class]]) {
+ NSDictionary* dict = val;
+ if (dict[kServerValueSubKey] != nil) {
+ NSString* serverValueType = [dict objectForKey:kServerValueSubKey];
+ if (serverValues[serverValueType] != nil) {
+ return [serverValues objectForKey:serverValueType];
+ } else {
+ // TODO: Throw unrecognizedServerValue error here
+ }
+ }
+ }
+ return val;
+}
+
++ (FCompoundWrite *) resolveDeferredValueCompoundWrite:(FCompoundWrite *)write withServerValues:(NSDictionary *)serverValues {
+ __block FCompoundWrite *resolved = write;
+ [write enumerateWrites:^(FPath *path, id<FNode> node, BOOL *stop) {
+ id<FNode> resolvedNode = [FServerValues resolveDeferredValueSnapshot:node withServerValues:serverValues];
+ // Node actually changed, use pointer inequality here
+ if (resolvedNode != node) {
+ resolved = [resolved addWrite:resolvedNode atPath:path];
+ }
+ }];
+ return resolved;
+}
+
++ (id) resolveDeferredValueTree:(FSparseSnapshotTree*)tree withServerValues:(NSDictionary*)serverValues {
+ FSparseSnapshotTree* resolvedTree = [[FSparseSnapshotTree alloc] init];
+ [tree forEachTreeAtPath:[FPath empty] do:^(FPath* path, id<FNode> node) {
+ [resolvedTree rememberData:[FServerValues resolveDeferredValueSnapshot:node withServerValues:serverValues] onPath:path];
+ }];
+ return resolvedTree;
+}
+
++ (id<FNode>) resolveDeferredValueSnapshot:(id<FNode>)node withServerValues:(NSDictionary*)serverValues {
+ id priorityVal = [FServerValues resolveDeferredValue:[[node getPriority] val] withServerValues:serverValues];
+ id<FNode> priority = [FSnapshotUtilities nodeFrom:priorityVal];
+
+ if ([node isLeafNode]) {
+ id value = [self resolveDeferredValue:[node val] withServerValues:serverValues];
+ if (![value isEqual:[node val]] || ![priority isEqual:[node getPriority]]) {
+ return [[FLeafNode alloc] initWithValue:value withPriority:priority];
+ } else {
+ return node;
+ }
+ } else {
+ __block FChildrenNode* newNode = node;
+ if (![priority isEqual:[node getPriority]]) {
+ newNode = [newNode updatePriority:priority];
+ }
+
+ [node enumerateChildrenUsingBlock:^(NSString *childKey, id<FNode> childNode, BOOL *stop) {
+ id newChildNode = [FServerValues resolveDeferredValueSnapshot:childNode withServerValues:serverValues];
+ if (![newChildNode isEqual:childNode]) {
+ newNode = [newNode updateImmediateChild:childKey withNewChild:newChildNode];
+ }
+ }];
+ return newNode;
+ }
+}
+
+@end
+
diff --git a/Firebase/Database/Core/FSnapshotHolder.h b/Firebase/Database/Core/FSnapshotHolder.h
new file mode 100644
index 0000000..9a1d871
--- /dev/null
+++ b/Firebase/Database/Core/FSnapshotHolder.h
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+#import "FNode.h"
+
+@interface FSnapshotHolder : NSObject
+
+- (id<FNode>) getNode:(FPath *)path;
+- (void) updateSnapshot:(FPath *)path withNewSnapshot:(id<FNode>)newSnapshotNode;
+
+@property (nonatomic, strong) id<FNode> rootNode;
+
+@end
diff --git a/Firebase/Database/Core/FSnapshotHolder.m b/Firebase/Database/Core/FSnapshotHolder.m
new file mode 100644
index 0000000..25c4625
--- /dev/null
+++ b/Firebase/Database/Core/FSnapshotHolder.m
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FSnapshotHolder.h"
+#import "FEmptyNode.h"
+
+@interface FSnapshotHolder()
+
+
+@end
+
+@implementation FSnapshotHolder
+
+@synthesize rootNode;
+
+- (id)init
+{
+ self = [super init];
+ if (self) {
+ self.rootNode = [FEmptyNode emptyNode];
+ }
+ return self;
+}
+
+- (id<FNode>) getNode:(FPath *)path {
+ return [self.rootNode getChild:path];
+}
+
+- (void) updateSnapshot:(FPath *)path withNewSnapshot:(id<FNode>)newSnapshotNode {
+ self.rootNode = [self.rootNode updateChild:path withNewChild:newSnapshotNode];
+}
+
+@end
diff --git a/Firebase/Database/Core/FSparseSnapshotTree.h b/Firebase/Database/Core/FSparseSnapshotTree.h
new file mode 100644
index 0000000..b860c9d
--- /dev/null
+++ b/Firebase/Database/Core/FSparseSnapshotTree.h
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+#import "FNode.h"
+#import "FPath.h"
+#import "FTypedefs_Private.h"
+
+@class FSparseSnapshotTree;
+
+typedef void (^fbt_void_nsstring_sstree) (NSString*, FSparseSnapshotTree*);
+
+@interface FSparseSnapshotTree : NSObject
+
+- (id<FNode>) findPath:(FPath *)path;
+- (void) rememberData:(id<FNode>)data onPath:(FPath *)path;
+- (BOOL) forgetPath:(FPath *)path;
+- (void) forEachTreeAtPath:(FPath *)prefixPath do:(fbt_void_path_node)func;
+- (void) forEachChild:(fbt_void_nsstring_sstree)func;
+
+@end
diff --git a/Firebase/Database/Core/FSparseSnapshotTree.m b/Firebase/Database/Core/FSparseSnapshotTree.m
new file mode 100644
index 0000000..1f16888
--- /dev/null
+++ b/Firebase/Database/Core/FSparseSnapshotTree.m
@@ -0,0 +1,144 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FSparseSnapshotTree.h"
+#import "FChildrenNode.h"
+
+@interface FSparseSnapshotTree () {
+ id<FNode> value;
+ NSMutableDictionary* children;
+}
+
+@end
+
+@implementation FSparseSnapshotTree
+
+- (id) init {
+ self = [super init];
+ if (self) {
+ value = nil;
+ children = nil;
+ }
+ return self;
+}
+
+- (id<FNode>) findPath:(FPath *)path {
+ if (value != nil) {
+ return [value getChild:path];
+ } else if (![path isEmpty] && children != nil) {
+ NSString* childKey = [path getFront];
+ path = [path popFront];
+ FSparseSnapshotTree* childTree = children[childKey];
+ if (childTree != nil) {
+ return [childTree findPath:path];
+ } else {
+ return nil;
+ }
+ } else {
+ return nil;
+ }
+}
+
+- (void) rememberData:(id<FNode>)data onPath:(FPath *)path {
+ if ([path isEmpty]) {
+ value = data;
+ children = nil;
+ } else if (value != nil) {
+ value = [value updateChild:path withNewChild:data];
+ } else {
+ if (children == nil) {
+ children = [[NSMutableDictionary alloc] init];
+ }
+
+ NSString* childKey = [path getFront];
+ if (children[childKey] == nil) {
+ children[childKey] = [[FSparseSnapshotTree alloc] init];
+ }
+
+ FSparseSnapshotTree* child = children[childKey];
+ path = [path popFront];
+ [child rememberData:data onPath:path];
+ }
+}
+
+- (BOOL) forgetPath:(FPath *)path {
+ if ([path isEmpty]) {
+ value = nil;
+ children = nil;
+ return YES;
+ } else {
+ if (value != nil) {
+ if ([value isLeafNode]) {
+ // non-empty path at leaf. the path leads to nowhere
+ return NO;
+ } else {
+ id<FNode> tmp = value;
+ value = nil;
+
+ [tmp enumerateChildrenUsingBlock:^(NSString *key, id<FNode> node, BOOL *stop) {
+ [self rememberData:node onPath:[[FPath alloc] initWith:key]];
+ }];
+
+ // we've cleared out the value and set children. Call ourself again to hit the next case
+ return [self forgetPath:path];
+ }
+ } else if (children != nil) {
+ NSString* childKey = [path getFront];
+ path = [path popFront];
+
+ if (children[childKey] != nil) {
+ FSparseSnapshotTree* child = children[childKey];
+ BOOL safeToRemove = [child forgetPath:path];
+ if (safeToRemove) {
+ [children removeObjectForKey:childKey];
+ }
+ }
+
+ if ([children count] == 0) {
+ children = nil;
+ return YES;
+ } else {
+ return NO;
+ }
+ } else {
+ return YES;
+ }
+ }
+}
+
+- (void) forEachTreeAtPath:(FPath *)prefixPath do:(fbt_void_path_node)func {
+ if (value != nil) {
+ func(prefixPath, value);
+ } else {
+ [self forEachChild:^(NSString* key, FSparseSnapshotTree* tree) {
+ FPath* path = [prefixPath childFromString:key];
+ [tree forEachTreeAtPath:path do:func];
+ }];
+ }
+}
+
+
+- (void) forEachChild:(fbt_void_nsstring_sstree)func {
+ if (children != nil) {
+ for (NSString* key in children) {
+ FSparseSnapshotTree* tree = [children objectForKey:key];
+ func(key, tree);
+ }
+ }
+}
+
+
+@end
diff --git a/Firebase/Database/Core/FSyncPoint.h b/Firebase/Database/Core/FSyncPoint.h
new file mode 100644
index 0000000..4e5a4e2
--- /dev/null
+++ b/Firebase/Database/Core/FSyncPoint.h
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+@protocol FOperation;
+@class FWriteTreeRef;
+@protocol FNode;
+@protocol FEventRegistration;
+@class FQuerySpec;
+@class FChildrenNode;
+@class FTupleRemovedQueriesEvents;
+@class FView;
+@class FPath;
+@class FCacheNode;
+@class FPersistenceManager;
+
+@interface FSyncPoint : NSObject
+
+- (id)initWithPersistenceManager:(FPersistenceManager *)persistence;
+
+- (BOOL) isEmpty;
+
+/**
+* Returns array of FEvent
+*/
+- (NSArray *) applyOperation:(id<FOperation>)operation writesCache:(FWriteTreeRef *)writesCache serverCache:(id<FNode>)optCompleteServerCache;
+
+/**
+* Returns array of FEvent
+*/
+- (NSArray *) addEventRegistration:(id <FEventRegistration>)eventRegistration
+ forNonExistingViewForQuery:(FQuerySpec *)query
+ writesCache:(FWriteTreeRef *)writesCache
+ serverCache:(FCacheNode *)serverCache;
+
+- (NSArray *) addEventRegistration:(id <FEventRegistration>)eventRegistration
+ forExistingViewForQuery:(FQuerySpec *)query;
+
+- (FTupleRemovedQueriesEvents *) removeEventRegistration:(id <FEventRegistration>)eventRegistration
+ forQuery:(FQuerySpec *)query
+ cancelError:(NSError *)cancelError;
+/**
+* Returns array of FViews
+*/
+- (NSArray *) queryViews;
+- (id<FNode>) completeServerCacheAtPath:(FPath *)path;
+- (FView *) viewForQuery:(FQuerySpec *)query;
+- (BOOL) viewExistsForQuery:(FQuerySpec *)query;
+- (BOOL) hasCompleteView;
+- (FView *) completeView;
+
+@end
diff --git a/Firebase/Database/Core/FSyncPoint.m b/Firebase/Database/Core/FSyncPoint.m
new file mode 100644
index 0000000..cd429f1
--- /dev/null
+++ b/Firebase/Database/Core/FSyncPoint.m
@@ -0,0 +1,257 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FSyncPoint.h"
+#import "FOperation.h"
+#import "FWriteTreeRef.h"
+#import "FNode.h"
+#import "FEventRegistration.h"
+#import "FIRDatabaseQuery.h"
+#import "FChildrenNode.h"
+#import "FTupleRemovedQueriesEvents.h"
+#import "FView.h"
+#import "FOperationSource.h"
+#import "FQuerySpec.h"
+#import "FQueryParams.h"
+#import "FPath.h"
+#import "FEmptyNode.h"
+#import "FViewCache.h"
+#import "FCacheNode.h"
+#import "FPersistenceManager.h"
+#import "FDataEvent.h"
+
+/**
+* SyncPoint represents a single location in a SyncTree with 1 or more event registrations, meaning we need to
+* maintain 1 or more Views at this location to cache server data and raise appropriate events for server changes
+* and user writes (set, transaction, update).
+*
+* It's responsible for:
+* - Maintaining the set of 1 or more views necessary at this location (a SyncPoint with 0 views should be removed).
+* - Proxying user / server operations to the views as appropriate (i.e. applyServerOverwrite,
+* applyUserOverwrite, etc.)
+*/
+@interface FSyncPoint ()
+/**
+* The Views being tracked at this location in the tree, stored as a map where the key is a
+* queryParams and the value is the View for that query.
+*
+* NOTE: This list will be quite small (usually 1, but perhaps 2 or 3; any more is an odd use case).
+*
+* Maps NSString -> FView
+*/
+@property (nonatomic, strong) NSMutableDictionary *views;
+
+@property (nonatomic, strong) FPersistenceManager *persistenceManager;
+@end
+
+@implementation FSyncPoint
+
+- (id) initWithPersistenceManager:(FPersistenceManager *)persistence {
+ self = [super init];
+ if (self) {
+ self.persistenceManager = persistence;
+ self.views = [[NSMutableDictionary alloc] init];
+ }
+ return self;
+}
+
+- (BOOL) isEmpty {
+ return [self.views count] == 0;
+}
+
+- (NSArray *) applyOperation:(id<FOperation>)operation
+ toView:(FView *)view
+ writesCache:(FWriteTreeRef *)writesCache
+ serverCache:(id<FNode>)optCompleteServerCache {
+ FViewOperationResult *result = [view applyOperation:operation writesCache:writesCache serverCache:optCompleteServerCache];
+ if (!view.query.loadsAllData) {
+ NSMutableSet *removed = [NSMutableSet set];
+ NSMutableSet *added = [NSMutableSet set];
+ [result.changes enumerateObjectsUsingBlock:^(FChange *change, NSUInteger idx, BOOL *stop) {
+ if (change.type == FIRDataEventTypeChildAdded) {
+ [added addObject:change.childKey];
+ } else if (change.type == FIRDataEventTypeChildRemoved) {
+ [removed addObject:change.childKey];
+ }
+ }];
+ if ([removed count] > 0 || [added count] > 0) {
+ [self.persistenceManager updateTrackedQueryKeysWithAddedKeys:added removedKeys:removed forQuery:view.query];
+ }
+ }
+ return result.events;
+}
+
+- (NSArray *) applyOperation:(id <FOperation>)operation writesCache:(FWriteTreeRef *)writesCache serverCache:(id <FNode>)optCompleteServerCache {
+ FQueryParams *queryParams = operation.source.queryParams;
+ if (queryParams != nil) {
+ FView *view = [self.views objectForKey:queryParams];
+ NSAssert(view != nil, @"SyncTree gave us an op for an invalid query.");
+ return [self applyOperation:operation toView:view writesCache:writesCache serverCache:optCompleteServerCache];
+ } else {
+ NSMutableArray *events = [[NSMutableArray alloc] init];
+ [self.views enumerateKeysAndObjectsUsingBlock:^(FQueryParams *key, FView *view, BOOL *stop) {
+ NSArray *eventsForView = [self applyOperation:operation toView:view writesCache:writesCache serverCache:optCompleteServerCache];
+ [events addObjectsFromArray:eventsForView];
+ }];
+ return events;
+ }
+}
+
+/**
+* Add an event callback for the specified query
+* Returns Array of FEvent events to raise.
+*/
+- (NSArray *) addEventRegistration:(id <FEventRegistration>)eventRegistration
+ forNonExistingViewForQuery:(FQuerySpec *)query
+ writesCache:(FWriteTreeRef *)writesCache
+ serverCache:(FCacheNode *)serverCache {
+ NSAssert(self.views[query.params] == nil, @"Found view for query: %@", query.params);
+ // TODO: make writesCache take flag for complete server node
+ id<FNode> eventCache = [writesCache calculateCompleteEventCacheWithCompleteServerCache:serverCache.isFullyInitialized ? serverCache.node : nil];
+ BOOL eventCacheComplete;
+ if (eventCache != nil) {
+ eventCacheComplete = YES;
+ } else {
+ eventCache = [writesCache calculateCompleteEventChildrenWithCompleteServerChildren:serverCache.node];
+ eventCacheComplete = NO;
+ }
+
+ FIndexedNode *indexed = [FIndexedNode indexedNodeWithNode:eventCache index:query.index];
+ FCacheNode *eventCacheNode = [[FCacheNode alloc] initWithIndexedNode:indexed
+ isFullyInitialized:eventCacheComplete
+ isFiltered:NO];
+ FViewCache *viewCache = [[FViewCache alloc] initWithEventCache:eventCacheNode serverCache:serverCache];
+ FView *view = [[FView alloc] initWithQuery:query initialViewCache:viewCache];
+ // If this is a non-default query we need to tell persistence our current view of the data
+ if (!query.loadsAllData) {
+ NSMutableSet *allKeys = [NSMutableSet set];
+ [view.eventCache enumerateChildrenUsingBlock:^(NSString *key, id<FNode> node, BOOL *stop) {
+ [allKeys addObject:key];
+ }];
+ [self.persistenceManager setTrackedQueryKeys:allKeys forQuery:query];
+ }
+ self.views[query.params] = view;
+ return [self addEventRegistration:eventRegistration forExistingViewForQuery:query];
+}
+
+- (NSArray *)addEventRegistration:(id<FEventRegistration>)eventRegistration
+ forExistingViewForQuery:(FQuerySpec *)query {
+ FView *view = self.views[query.params];
+ NSAssert(view != nil, @"No view for query: %@", query);
+ [view addEventRegistration:eventRegistration];
+ return [view initialEvents:eventRegistration];
+}
+
+/**
+* Remove event callback(s). Return cancelEvents if a cancelError is specified.
+*
+* If query is the default query, we'll check all views for the specified eventRegistration.
+* If eventRegistration is nil, we'll remove all callbacks for the specified view(s).
+*
+* @return FTupleRemovedQueriesEvents removed queries and any cancel events
+*/
+- (FTupleRemovedQueriesEvents *) removeEventRegistration:(id <FEventRegistration>)eventRegistration
+ forQuery:(FQuerySpec *)query
+ cancelError:(NSError *)cancelError {
+ NSMutableArray *removedQueries = [[NSMutableArray alloc] init];
+ __block NSMutableArray *cancelEvents = [[NSMutableArray alloc] init];
+ BOOL hadCompleteView = [self hasCompleteView];
+ if ([query isDefault]) {
+ // When you do [ref removeObserverWithHandle:], we search all views for the registration to remove.
+ [self.views enumerateKeysAndObjectsUsingBlock:^(FQueryParams *viewQueryParams, FView *view, BOOL *stop) {
+ [cancelEvents addObjectsFromArray:[view removeEventRegistration:eventRegistration cancelError:cancelError]];
+ if ([view isEmpty]) {
+ [self.views removeObjectForKey:viewQueryParams];
+
+ // We'll deal with complete views later
+ if (![view.query loadsAllData]) {
+ [removedQueries addObject:view.query];
+ }
+ }
+ }];
+ } else {
+ // remove the callback from the specific view
+ FView *view = [self.views objectForKey:query.params];
+ if (view != nil) {
+ [cancelEvents addObjectsFromArray:[view removeEventRegistration:eventRegistration cancelError:cancelError]];
+
+ if ([view isEmpty]) {
+ [self.views removeObjectForKey:query.params];
+
+ // We'll deal with complete views later
+ if (![view.query loadsAllData]) {
+ [removedQueries addObject:view.query];
+ }
+ }
+ }
+ }
+
+ if (hadCompleteView && ![self hasCompleteView]) {
+ // We removed our last complete view
+ [removedQueries addObject:[FQuerySpec defaultQueryAtPath:query.path]];
+ }
+
+ return [[FTupleRemovedQueriesEvents alloc] initWithRemovedQueries:removedQueries cancelEvents:cancelEvents];
+}
+
+- (NSArray *) queryViews {
+ __block NSMutableArray *filteredViews = [[NSMutableArray alloc] init];
+
+ [self.views enumerateKeysAndObjectsUsingBlock:^(FQueryParams *key, FView *view, BOOL *stop) {
+ if (![view.query loadsAllData]) {
+ [filteredViews addObject:view];
+ }
+ }];
+
+ return filteredViews;
+}
+
+- (id <FNode>) completeServerCacheAtPath:(FPath *)path {
+ __block id<FNode> serverCache = nil;
+ [self.views enumerateKeysAndObjectsUsingBlock:^(FQueryParams *key, FView *view, BOOL *stop) {
+ serverCache = [view completeServerCacheFor:path];
+ *stop = (serverCache != nil);
+ }];
+ return serverCache;
+}
+
+- (FView *) viewForQuery:(FQuerySpec *)query {
+ return [self.views objectForKey:query.params];
+}
+
+- (BOOL) viewExistsForQuery:(FQuerySpec *)query {
+ return [self viewForQuery:query] != nil;
+}
+
+- (BOOL) hasCompleteView {
+ return [self completeView] != nil;
+}
+
+- (FView *) completeView {
+ __block FView *completeView = nil;
+
+ [self.views enumerateKeysAndObjectsUsingBlock:^(FQueryParams *key, FView *view, BOOL *stop) {
+ if ([view.query loadsAllData]) {
+ completeView = view;
+ *stop = YES;
+ }
+ }];
+
+ return completeView;
+}
+
+
+@end
diff --git a/Firebase/Database/Core/FSyncTree.h b/Firebase/Database/Core/FSyncTree.h
new file mode 100644
index 0000000..887f721
--- /dev/null
+++ b/Firebase/Database/Core/FSyncTree.h
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+@class FListenProvider;
+@protocol FNode;
+@class FPath;
+@protocol FEventRegistration;
+@protocol FPersistedServerCache;
+@class FQuerySpec;
+@class FCompoundWrite;
+@class FPersistenceManager;
+@class FCompoundHash;
+@protocol FClock;
+
+@protocol FSyncTreeHash <NSObject>
+
+- (NSString *)simpleHash;
+- (FCompoundHash *)compoundHash;
+- (BOOL)includeCompoundHash;
+
+@end
+
+@interface FSyncTree : NSObject
+
+- (id) initWithListenProvider:(FListenProvider *)provider;
+- (id) initWithPersistenceManager:(FPersistenceManager *)persistenceManager
+ listenProvider:(FListenProvider *)provider;
+
+// These methods all return NSArray of FEvent
+- (NSArray *) applyUserOverwriteAtPath:(FPath *)path newData:(id <FNode>)newData writeId:(NSInteger)writeId isVisible:(BOOL)visible;
+- (NSArray *) applyUserMergeAtPath:(FPath *)path changedChildren:(FCompoundWrite *)changedChildren writeId:(NSInteger)writeId;
+- (NSArray *) ackUserWriteWithWriteId:(NSInteger)writeId revert:(BOOL)revert persist:(BOOL)persist clock:(id<FClock>)clock;
+- (NSArray *) applyServerOverwriteAtPath:(FPath *)path newData:(id<FNode>)newData;
+- (NSArray *) applyServerMergeAtPath:(FPath *)path changedChildren:(FCompoundWrite *)changedChildren;
+- (NSArray *) applyServerRangeMergeAtPath:(FPath *)path updates:(NSArray *)ranges;
+- (NSArray *) applyTaggedQueryOverwriteAtPath:(FPath *)path newData:(id <FNode>)newData tagId:(NSNumber *)tagId;
+- (NSArray *) applyTaggedQueryMergeAtPath:(FPath *)path changedChildren:(FCompoundWrite *)changedChildren tagId:(NSNumber *)tagId;
+- (NSArray *) applyTaggedServerRangeMergeAtPath:(FPath *)path updates:(NSArray *)ranges tagId:(NSNumber *)tagId;
+- (NSArray *) addEventRegistration:(id<FEventRegistration>)eventRegistration forQuery:(FQuerySpec *)query;
+- (NSArray *) removeEventRegistration:(id <FEventRegistration>)eventRegistration forQuery:(FQuerySpec *)query cancelError:(NSError *)cancelError;
+- (void)keepQuery:(FQuerySpec *)query synced:(BOOL)keepSynced;
+- (NSArray *) removeAllWrites;
+
+- (id<FNode>) calcCompleteEventCacheAtPath:(FPath *)path excludeWriteIds:(NSArray *)writeIdsToExclude;
+
+@end
diff --git a/Firebase/Database/Core/FSyncTree.m b/Firebase/Database/Core/FSyncTree.m
new file mode 100644
index 0000000..37100c1
--- /dev/null
+++ b/Firebase/Database/Core/FSyncTree.m
@@ -0,0 +1,817 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FSyncTree.h"
+#import "FListenProvider.h"
+#import "FWriteTree.h"
+#import "FNode.h"
+#import "FPath.h"
+#import "FEventRegistration.h"
+#import "FImmutableTree.h"
+#import "FOperation.h"
+#import "FWriteTreeRef.h"
+#import "FOverwrite.h"
+#import "FOperationSource.h"
+#import "FMerge.h"
+#import "FAckUserWrite.h"
+#import "FView.h"
+#import "FSyncPoint.h"
+#import "FEmptyNode.h"
+#import "FQueryParams.h"
+#import "FQuerySpec.h"
+#import "FSnapshotHolder.h"
+#import "FChildrenNode.h"
+#import "FTupleRemovedQueriesEvents.h"
+#import "FAtomicNumber.h"
+#import "FEventRaiser.h"
+#import "FListenComplete.h"
+#import "FSnapshotUtilities.h"
+#import "FCacheNode.h"
+#import "FUtilities.h"
+#import "FCompoundWrite.h"
+#import "FWriteRecord.h"
+#import "FPersistenceManager.h"
+#import "FKeepSyncedEventRegistration.h"
+#import "FServerValues.h"
+#import "FCompoundHash.h"
+#import "FRangeMerge.h"
+
+// Size after which we start including the compound hash
+static const NSUInteger kFSizeThresholdForCompoundHash = 1024;
+
+@interface FListenContainer : NSObject<FSyncTreeHash>
+
+@property (nonatomic, strong) FView *view;
+@property (nonatomic, copy) fbt_nsarray_nsstring onComplete;
+
+@end
+
+@implementation FListenContainer
+
+- (instancetype)initWithView:(FView *)view onComplete:(fbt_nsarray_nsstring)onComplete {
+ self = [super init];
+ if (self != nil) {
+ self->_view = view;
+ self->_onComplete = onComplete;
+ }
+ return self;
+}
+
+- (id<FNode>)serverCache {
+ return self.view.serverCache;
+}
+
+- (FCompoundHash *)compoundHash {
+ return [FCompoundHash fromNode:[self serverCache]];
+}
+
+- (NSString *)simpleHash {
+ return [[self serverCache] dataHash];
+}
+
+- (BOOL)includeCompoundHash {
+ return [FSnapshotUtilities estimateSerializedNodeSize:[self serverCache]] > kFSizeThresholdForCompoundHash;
+}
+
+@end
+
+@interface FSyncTree ()
+
+/**
+* Tree of SyncPoints. There's a SyncPoint at any location that has 1 or more views.
+*/
+@property (nonatomic, strong) FImmutableTree *syncPointTree;
+
+/**
+* A tree of all pending user writes (user-initiated set, transactions, updates, etc)
+*/
+@property (nonatomic, strong) FWriteTree *pendingWriteTree;
+
+/**
+* Maps tagId -> FTuplePathQueryParams
+*/
+@property (nonatomic, strong) NSMutableDictionary *tagToQueryMap;
+@property (nonatomic, strong) NSMutableDictionary *queryToTagMap;
+@property (nonatomic, strong) FListenProvider *listenProvider;
+@property (nonatomic, strong) FPersistenceManager *persistenceManager;
+@property (nonatomic, strong) FAtomicNumber *queryTagCounter;
+@property (nonatomic, strong) NSMutableSet *keepSyncedQueries;
+
+@end
+
+/**
+* SyncTree is the central class for managing event callback registration, data caching, views
+* (query processing), and event generation. There are typically two SyncTree instances for
+* each Repo, one for the normal Firebase data, and one for the .info data.
+*
+* It has a number of responsibilities, including:
+* - Tracking all user event callbacks (registered via addEventRegistration: and removeEventRegistration:).
+* - Applying and caching data changes for user setValue:, runTransactionBlock:, and updateChildValues: calls
+* (applyUserOverwriteAtPath:, applyUserMergeAtPath:).
+* - Applying and caching data changes for server data changes (applyServerOverwriteAtPath:,
+* applyServerMergeAtPath:).
+* - Generating user-facing events for server and user changes (all of the apply* methods
+* return the set of events that need to be raised as a result).
+* - Maintaining the appropriate set of server listens to ensure we are always subscribed
+* to the correct set of paths and queries to satisfy the current set of user event
+* callbacks (listens are started/stopped using the provided listenProvider).
+*
+* NOTE: Although SyncTree tracks event callbacks and calculates events to raise, the actual
+* events are returned to the caller rather than raised synchronously.
+*/
+@implementation FSyncTree
+
+- (id) initWithListenProvider:(FListenProvider *)provider {
+ return [self initWithPersistenceManager:nil listenProvider:provider];
+}
+
+- (id) initWithPersistenceManager:(FPersistenceManager *)persistenceManager listenProvider:(FListenProvider *)provider {
+ self = [super init];
+ if (self) {
+ self.syncPointTree = [FImmutableTree empty];
+ self.pendingWriteTree = [[FWriteTree alloc] init];
+ self.tagToQueryMap = [[NSMutableDictionary alloc] init];
+ self.queryToTagMap = [[NSMutableDictionary alloc] init];
+ self.listenProvider = provider;
+ self.persistenceManager = persistenceManager;
+ self.queryTagCounter = [[FAtomicNumber alloc] init];
+ self.keepSyncedQueries = [NSMutableSet set];
+ }
+ return self;
+}
+
+#pragma mark -
+#pragma mark Apply Operations
+
+/**
+* Apply data changes for a user-generated setValue: runTransactionBlock: updateChildValues:, etc.
+* @return NSArray of FEvent to raise.
+*/
+- (NSArray *) applyUserOverwriteAtPath:(FPath *)path newData:(id <FNode>)newData writeId:(NSInteger)writeId isVisible:(BOOL)visible {
+ // Record pending write
+ [self.pendingWriteTree addOverwriteAtPath:path newData:newData writeId:writeId isVisible:visible];
+ if (!visible) {
+ return @[];
+ } else {
+ FOverwrite *operation = [[FOverwrite alloc] initWithSource:[FOperationSource userInstance] path:path snap:newData];
+ return [self applyOperationToSyncPoints:operation];
+ }
+}
+
+/**
+* Apply the data from a user-generated updateChildValues: call
+* @return NSArray of FEvent to raise.
+*/
+- (NSArray *) applyUserMergeAtPath:(FPath *)path changedChildren:(FCompoundWrite *)changedChildren writeId:(NSInteger)writeId {
+ // Record pending merge
+ [self.pendingWriteTree addMergeAtPath:path changedChildren:changedChildren writeId:writeId];
+
+ FMerge *operation = [[FMerge alloc] initWithSource:[FOperationSource userInstance] path:path children:changedChildren];
+ return [self applyOperationToSyncPoints:operation];
+}
+
+/**
+ * Acknowledge a pending user write that was previously registered with applyUserOverwriteAtPath: or applyUserMergeAtPath:
+ * TODO[offline]: Taking a serverClock here is awkward, but server values are awkward. :-(
+ * @return NSArray of FEvent to raise.
+ */
+- (NSArray *) ackUserWriteWithWriteId:(NSInteger)writeId revert:(BOOL)revert persist:(BOOL)persist clock:(id<FClock>)clock {
+ FWriteRecord *write = [self.pendingWriteTree writeForId:writeId];
+ BOOL needToReevaluate = [self.pendingWriteTree removeWriteId:writeId];
+ if (write.visible) {
+ if (persist) {
+ [self.persistenceManager removeUserWrite:writeId];
+ }
+ if (!revert) {
+ NSDictionary *serverValues = [FServerValues generateServerValues:clock];
+ if ([write isOverwrite]) {
+ id<FNode> resolvedNode = [FServerValues resolveDeferredValueSnapshot:write.overwrite withServerValues:serverValues];
+ [self.persistenceManager applyUserWrite:resolvedNode toServerCacheAtPath:write.path];
+ } else {
+ FCompoundWrite *resolvedMerge = [FServerValues resolveDeferredValueCompoundWrite:write.merge withServerValues:serverValues];
+ [self.persistenceManager applyUserMerge:resolvedMerge toServerCacheAtPath:write.path];
+ }
+ }
+ }
+ if (!needToReevaluate) {
+ return @[];
+ } else {
+ __block FImmutableTree *affectedTree = [FImmutableTree empty];
+ if (write.isOverwrite) {
+ affectedTree = [affectedTree setValue:@YES atPath:[FPath empty]];
+ } else {
+ [write.merge enumerateWrites:^(FPath *path, id <FNode> node, BOOL *stop) {
+ affectedTree = [affectedTree setValue:@YES atPath:path];
+ }];
+ }
+ FAckUserWrite *operation = [[FAckUserWrite alloc] initWithPath:write.path affectedTree:affectedTree revert:revert];
+ return [self applyOperationToSyncPoints:operation];
+ }
+}
+
+/**
+* Apply new server data for the specified path
+* @return NSArray of FEvent to raise.
+*/
+- (NSArray *) applyServerOverwriteAtPath:(FPath *)path newData:(id <FNode>)newData {
+ [self.persistenceManager updateServerCacheWithNode:newData forQuery:[FQuerySpec defaultQueryAtPath:path]];
+ FOverwrite *operation = [[FOverwrite alloc] initWithSource:[FOperationSource serverInstance] path:path snap:newData];
+ return [self applyOperationToSyncPoints:operation];
+}
+
+/**
+* Applied new server data to be merged in at the specified path
+* @return NSArray of FEvent to raise.
+*/
+- (NSArray *) applyServerMergeAtPath:(FPath *)path changedChildren:(FCompoundWrite *)changedChildren {
+ [self.persistenceManager updateServerCacheWithMerge:changedChildren atPath:path];
+ FMerge *operation = [[FMerge alloc] initWithSource:[FOperationSource serverInstance] path:path children:changedChildren];
+ return [self applyOperationToSyncPoints:operation];
+}
+
+- (NSArray *) applyServerRangeMergeAtPath:(FPath *)path updates:(NSArray *)ranges {
+ FSyncPoint *syncPoint = [self.syncPointTree valueAtPath:path];
+ if (syncPoint == nil) {
+ // Removed view, so it's safe to just ignore this update
+ return @[];
+ } else {
+ // This could be for any "complete" (unfiltered) view, and if there is more than one complete view, they should
+ // each have the same cache so it doesn't matter which one we use.
+ FView *view = [syncPoint completeView];
+ if (view != nil) {
+ id<FNode> serverNode = [view serverCache];
+ for (FRangeMerge *merge in ranges) {
+ serverNode = [merge applyToNode:serverNode];
+ }
+ return [self applyServerOverwriteAtPath:path newData:serverNode];
+ } else {
+ // There doesn't exist a view for this update, so it was removed and it's safe to just ignore this range
+ // merge
+ return @[];
+ }
+ }
+}
+
+/**
+* Apply a listen complete to a path
+* @return NSArray of FEvent to raise.
+*/
+- (NSArray *) applyListenCompleteAtPath:(FPath *)path {
+ [self.persistenceManager setQueryComplete:[FQuerySpec defaultQueryAtPath:path]];
+ id<FOperation> operation = [[FListenComplete alloc] initWithSource:[FOperationSource serverInstance] path:path];
+ return [self applyOperationToSyncPoints:operation];
+}
+
+/**
+* Apply a listen complete to a path
+* @return NSArray of FEvent to raise.
+*/
+- (NSArray *) applyTaggedListenCompleteAtPath:(FPath *)path tagId:(NSNumber *)tagId {
+ FQuerySpec *query = [self queryForTag:tagId];
+ if (query != nil) {
+ [self.persistenceManager setQueryComplete:query];
+ FPath *relativePath = [FPath relativePathFrom:query.path to:path];
+ id<FOperation> op = [[FListenComplete alloc] initWithSource:[FOperationSource forServerTaggedQuery:query.params]
+ path:relativePath];
+ return [self applyTaggedOperation:op atPath:query.path];
+ } else {
+ // We've already removed the query. No big deal, ignore the update.
+ return @[];
+ }
+}
+
+/**
+* Internal helper method to apply tagged operation
+*/
+- (NSArray *) applyTaggedOperation:(id<FOperation>)operation atPath:(FPath *)path {
+ FSyncPoint *syncPoint = [self.syncPointTree valueAtPath:path];
+ NSAssert(syncPoint != nil, @"Missing sync point for query tag that we're tracking.");
+ FWriteTreeRef *writesCache = [self.pendingWriteTree childWritesForPath:path];
+ return [syncPoint applyOperation:operation writesCache:writesCache serverCache:nil];
+}
+
+/**
+* Apply new server data for the specified tagged query
+* @return NSArray of FEvent to raise.
+*/
+- (NSArray *) applyTaggedQueryOverwriteAtPath:(FPath *)path newData:(id <FNode>)newData tagId:(NSNumber *)tagId {
+ FQuerySpec *query = [self queryForTag:tagId];
+ if (query != nil) {
+ FPath *relativePath = [FPath relativePathFrom:query.path to:path];
+ FQuerySpec *queryToOverwrite = relativePath.isEmpty ? query : [FQuerySpec defaultQueryAtPath:path];
+ [self.persistenceManager updateServerCacheWithNode:newData forQuery:queryToOverwrite];
+ FOverwrite *operation = [[FOverwrite alloc] initWithSource:[FOperationSource forServerTaggedQuery:query.params]
+ path:relativePath snap:newData];
+ return [self applyTaggedOperation:operation atPath:query.path];
+ } else {
+ // Query must have been removed already
+ return @[];
+ }
+}
+
+/**
+* Apply server data to be merged in for the specified tagged query
+* @return NSArray of FEvent to raise.
+*/
+- (NSArray *) applyTaggedQueryMergeAtPath:(FPath *)path changedChildren:(FCompoundWrite *)changedChildren tagId:(NSNumber *)tagId {
+ FQuerySpec *query = [self queryForTag:tagId];
+ if (query != nil) {
+ FPath *relativePath = [FPath relativePathFrom:query.path to:path];
+ [self.persistenceManager updateServerCacheWithMerge:changedChildren atPath:path];
+ FMerge *operation = [[FMerge alloc] initWithSource:[FOperationSource forServerTaggedQuery:query.params]
+ path:relativePath
+ children:changedChildren];
+ return [self applyTaggedOperation:operation atPath:query.path];
+ } else {
+ // We've already removed the query. No big deal, ignore the update.
+ return @[];
+ }
+}
+
+- (NSArray *) applyTaggedServerRangeMergeAtPath:(FPath *)path updates:(NSArray *)ranges tagId:(NSNumber *)tagId {
+ FQuerySpec *query = [self queryForTag:tagId];
+ if (query != nil) {
+ NSAssert([path isEqual:query.path], @"Tagged update path and query path must match");
+ FSyncPoint *syncPoint = [self.syncPointTree valueAtPath:path];
+ NSAssert(syncPoint != nil, @"Missing sync point for query tag that we're tracking.");
+ FView *view = [syncPoint viewForQuery:query];
+ NSAssert(view != nil, @"Missing view for query tag that we're tracking");
+ id<FNode> serverNode = [view serverCache];
+ for (FRangeMerge *merge in ranges) {
+ serverNode = [merge applyToNode:serverNode];
+ }
+ return [self applyTaggedQueryOverwriteAtPath:path newData:serverNode tagId:tagId];
+ } else {
+ // We've already removed the query. No big deal, ignore the update.
+ return @[];
+ }
+}
+
+/**
+* Add an event callback for the specified query
+* @return NSArray of FEvent to raise.
+*/
+- (NSArray *) addEventRegistration:(id<FEventRegistration>)eventRegistration forQuery:(FQuerySpec *)query {
+ FPath *path = query.path;
+
+ __block BOOL foundAncestorDefaultView = NO;
+ [self.syncPointTree forEachOnPath:query.path whileBlock:^BOOL(FPath *pathToSyncPoint, FSyncPoint *syncPoint) {
+ foundAncestorDefaultView = foundAncestorDefaultView || [syncPoint hasCompleteView];
+ return !foundAncestorDefaultView;
+ }];
+
+ [self.persistenceManager setQueryActive:query];
+
+ FSyncPoint *syncPoint = [self.syncPointTree valueAtPath:path];
+ if (syncPoint == nil) {
+ syncPoint = [[FSyncPoint alloc] initWithPersistenceManager:self.persistenceManager];
+ self.syncPointTree = [self.syncPointTree setValue:syncPoint atPath:path];
+ }
+
+ BOOL viewAlreadyExists = [syncPoint viewExistsForQuery:query];
+ NSArray *events;
+ if (viewAlreadyExists) {
+ events = [syncPoint addEventRegistration:eventRegistration forExistingViewForQuery:query];
+ } else {
+ if (![query loadsAllData]) {
+ // We need to track a tag for this query
+ NSAssert(self.queryToTagMap[query] == nil, @"View does not exist, but we have a tag");
+ NSNumber *tagId = [self.queryTagCounter getAndIncrement];
+ self.queryToTagMap[query] = tagId;
+ self.tagToQueryMap[tagId] = query;
+ }
+
+ FWriteTreeRef *writesCache = [self.pendingWriteTree childWritesForPath:path];
+ FCacheNode *serverCache = [self serverCacheForQuery:query];
+ events = [syncPoint addEventRegistration:eventRegistration
+ forNonExistingViewForQuery:query
+ writesCache:writesCache
+ serverCache:serverCache];
+
+ // There was no view and no default listen
+ if (!foundAncestorDefaultView) {
+ FView *view = [syncPoint viewForQuery:query];
+ NSMutableArray *mutableEvents = [events mutableCopy];
+ [mutableEvents addObjectsFromArray:[self setupListenerOnQuery:query view:view]];
+ events = mutableEvents;
+ }
+ }
+
+ return events;
+}
+
+- (FCacheNode *)serverCacheForQuery:(FQuerySpec *)query {
+ __block id<FNode> serverCacheNode = nil;
+
+ [self.syncPointTree forEachOnPath:query.path whileBlock:^BOOL(FPath *pathToSyncPoint, FSyncPoint *syncPoint) {
+ FPath *relativePath = [FPath relativePathFrom:pathToSyncPoint to:query.path];
+ serverCacheNode = [syncPoint completeServerCacheAtPath:relativePath];
+ return serverCacheNode == nil;
+ }];
+
+ FCacheNode *serverCache;
+ if (serverCacheNode != nil) {
+ FIndexedNode *indexed = [FIndexedNode indexedNodeWithNode:serverCacheNode index:query.index];
+ serverCache = [[FCacheNode alloc] initWithIndexedNode:indexed isFullyInitialized:YES isFiltered:NO];
+ } else {
+ FCacheNode *persistenceServerCache = [self.persistenceManager serverCacheForQuery:query];
+ if (persistenceServerCache.isFullyInitialized) {
+ serverCache = persistenceServerCache;
+ } else {
+ serverCacheNode = [FEmptyNode emptyNode];
+
+ FImmutableTree *subtree = [self.syncPointTree subtreeAtPath:query.path];
+ [subtree forEachChild:^(NSString *childKey, FSyncPoint *childSyncPoint) {
+ id<FNode> completeCache = [childSyncPoint completeServerCacheAtPath:[FPath empty]];
+ if (completeCache) {
+ serverCacheNode = [serverCacheNode updateImmediateChild:childKey withNewChild:completeCache];
+ }
+ }];
+ // Fill the node with any available children we have
+ [persistenceServerCache.node enumerateChildrenUsingBlock:^(NSString *key, id<FNode> node, BOOL *stop) {
+ if (![serverCacheNode hasChild:key]) {
+ serverCacheNode = [serverCacheNode updateImmediateChild:key withNewChild:node];
+ }
+ }];
+ FIndexedNode *indexed = [FIndexedNode indexedNodeWithNode:serverCacheNode index:query.index];
+ serverCache = [[FCacheNode alloc] initWithIndexedNode:indexed isFullyInitialized:NO isFiltered:NO];
+ }
+ }
+
+ return serverCache;
+}
+
+/**
+* Remove event callback(s).
+*
+* If query is the default query, we'll check all queries for the specified eventRegistration.
+* If eventRegistration is null, we'll remove all callbacks for the specified query/queries.
+*
+* @param eventRegistration if nil, all callbacks are removed
+* @param cancelError If provided, appropriate cancel events will be returned
+* @return NSArray of FEvent to raise.
+*/
+- (NSArray *) removeEventRegistration:(id <FEventRegistration>)eventRegistration
+ forQuery:(FQuerySpec *)query
+ cancelError:(NSError *)cancelError {
+ // Find the syncPoint first. Then deal with whether or not it has matching listeners
+ FPath *path = query.path;
+ FSyncPoint *maybeSyncPoint = [self.syncPointTree valueAtPath:path];
+ NSArray *cancelEvents = @[];
+
+ // A removal on a default query affects all queries at that location. A removal on an indexed query, even one without
+ // other query constraints, does *not* affect all queries at that location. So this check must be for 'default', and
+ // not loadsAllData:
+ if (maybeSyncPoint && ([query isDefault] || [maybeSyncPoint viewExistsForQuery:query])) {
+ FTupleRemovedQueriesEvents *removedAndEvents = [maybeSyncPoint removeEventRegistration:eventRegistration forQuery:query cancelError:cancelError];
+ if ([maybeSyncPoint isEmpty]) {
+ self.syncPointTree = [self.syncPointTree removeValueAtPath:path];
+ }
+ NSArray *removed = removedAndEvents.removedQueries;
+ cancelEvents = removedAndEvents.cancelEvents;
+
+ // We may have just removed one of many listeners and can short-circuit this whole process
+ // We may also not have removed a default listener, in which case all of the descendant listeners should already
+ // be properly set up.
+ //
+ // Since indexed queries can shadow if they don't have other query constraints, check for loadsAllData: instead
+ // of isDefault:
+ NSUInteger defaultQueryIndex = [removed indexOfObjectPassingTest:^BOOL(FQuerySpec *q, NSUInteger idx, BOOL *stop) {
+ return [q loadsAllData];
+ }];
+ BOOL removingDefault = defaultQueryIndex != NSNotFound;
+ [removed enumerateObjectsUsingBlock:^(FQuerySpec *query, NSUInteger idx, BOOL *stop) {
+ [self.persistenceManager setQueryInactive:query];
+ }];
+ NSNumber *covered = [self.syncPointTree findOnPath:path andApplyBlock:^id(FPath *relativePath, FSyncPoint *parentSyncPoint) {
+ return [NSNumber numberWithBool:[parentSyncPoint hasCompleteView]];
+ }];
+
+ if (removingDefault && ![covered boolValue]) {
+ FImmutableTree *subtree = [self.syncPointTree subtreeAtPath:path];
+ // There are potentially child listeners. Determine what if any listens we need to send before executing
+ // the removal
+ if (![subtree isEmpty]) {
+ // We need to fold over our subtree and collect the listeners to send
+ NSArray *newViews = [self collectDistinctViewsForSubTree:subtree];
+
+ // Ok, we've collected all the listens we need. Set them up.
+ [newViews enumerateObjectsUsingBlock:^(FView *view, NSUInteger idx, BOOL *stop) {
+ FQuerySpec *newQuery = view.query;
+ FListenContainer *listenContainer = [self createListenerForView:view];
+ self.listenProvider.startListening([self queryForListening:newQuery], [self tagForQuery:newQuery],
+ listenContainer, listenContainer.onComplete);
+ }];
+ } else {
+ // There's nothing below us, so nothing we need to start listening on
+ }
+ }
+
+ // If we removed anything and we're not covered by a higher up listen, we need to stop listening on this query.
+ // The above block has us covered in terms of making sure we're set up on listens lower in the tree.
+ // Also, note that if we have a cancelError, it's already been removed at the provider level.
+ if (![covered boolValue] && [removed count] > 0 && cancelError == nil) {
+ // If we removed a default, then we weren't listening on any of the other queries here. Just cancel the one
+ // default. Otherwise, we need to iterate through and cancel each individual query
+ if (removingDefault) {
+ // We don't tag default listeners
+ self.listenProvider.stopListening([self queryForListening:query], nil);
+ } else {
+ [removed enumerateObjectsUsingBlock:^(FQuerySpec *queryToRemove, NSUInteger idx, BOOL *stop) {
+ NSNumber *tagToRemove = [self.queryToTagMap objectForKey:queryToRemove];
+ self.listenProvider.stopListening([self queryForListening:queryToRemove], tagToRemove);
+ }];
+ }
+ }
+ // Now, clear all the tags we're tracking for the removed listens.
+ [self removeTags:removed];
+ } else {
+ // No-op, this listener must've been already removed
+ }
+ return cancelEvents;
+}
+
+- (void)keepQuery:(FQuerySpec *)query synced:(BOOL)keepSynced {
+ // Only do something if we actually need to add/remove an event registration
+ if (keepSynced && ![self.keepSyncedQueries containsObject:query]) {
+ [self addEventRegistration:[FKeepSyncedEventRegistration instance] forQuery:query];
+ [self.keepSyncedQueries addObject:query];
+ } else if (!keepSynced && [self.keepSyncedQueries containsObject:query]) {
+ [self removeEventRegistration:[FKeepSyncedEventRegistration instance] forQuery:query cancelError:nil];
+ [self.keepSyncedQueries removeObject:query];
+ }
+}
+
+- (NSArray *) removeAllWrites {
+ [self.persistenceManager removeAllUserWrites];
+ NSArray *removedWrites = [self.pendingWriteTree removeAllWrites];
+ if (removedWrites.count > 0) {
+ FImmutableTree *affectedTree = [[FImmutableTree empty] setValue:@YES atPath:[FPath empty]];
+ return [self applyOperationToSyncPoints:[[FAckUserWrite alloc] initWithPath:[FPath empty]
+ affectedTree:affectedTree revert:YES]];
+ } else {
+ return @[];
+ }
+}
+
+/**
+* Returns a complete cache, if we have one, of the data at a particular path. The location must have a listener above
+* it, but as this is only used by transaction code, that should always be the case anyways.
+*
+* Note: this method will *include* hidden writes from transaction with applyLocally set to false.
+* @param path The path to the data we want
+* @param writeIdsToExclude A specific set to be excluded
+*/
+- (id <FNode>) calcCompleteEventCacheAtPath:(FPath *)path excludeWriteIds:(NSArray *)writeIdsToExclude {
+ BOOL includeHiddenSets = YES;
+ FWriteTree *writeTree = self.pendingWriteTree;
+ id<FNode> serverCache = [self.syncPointTree findOnPath:path andApplyBlock:^id<FNode>(FPath *pathSoFar, FSyncPoint *syncPoint) {
+ FPath *relativePath = [FPath relativePathFrom:pathSoFar to:path];
+ id<FNode> serverCache = [syncPoint completeServerCacheAtPath:relativePath];
+ if (serverCache) {
+ return serverCache;
+ } else {
+ return nil;
+ }
+ }];
+ return [writeTree calculateCompleteEventCacheAtPath:path completeServerCache:serverCache excludeWriteIds:writeIdsToExclude includeHiddenWrites:includeHiddenSets];
+}
+
+#pragma mark -
+#pragma mark Private Methods
+/**
+* This collapses multiple unfiltered views into a single view, since we only need a single
+* listener for them.
+* @return NSArray of FView
+*/
+- (NSArray *) collectDistinctViewsForSubTree:(FImmutableTree *)subtree {
+ return [subtree foldWithBlock:^NSArray *(FPath *relativePath, FSyncPoint *maybeChildSyncPoint, NSDictionary *childMap) {
+ if (maybeChildSyncPoint && [maybeChildSyncPoint hasCompleteView]) {
+ FView *completeView = [maybeChildSyncPoint completeView];
+ return @[completeView];
+ } else {
+ // No complete view here, flatten any deeper listens into an array
+ NSMutableArray *views = [[NSMutableArray alloc] init];
+ if (maybeChildSyncPoint) {
+ views = [[maybeChildSyncPoint queryViews] mutableCopy];
+ }
+ [childMap enumerateKeysAndObjectsUsingBlock:^(NSString *childKey, NSArray *childViews, BOOL *stop) {
+ [views addObjectsFromArray:childViews];
+ }];
+ return views;
+ }
+ }];
+}
+
+/**
+* @param queries NSArray of FQuerySpec
+*/
+- (void) removeTags:(NSArray *)queries {
+ [queries enumerateObjectsUsingBlock:^(FQuerySpec *removedQuery, NSUInteger idx, BOOL *stop) {
+ if (![removedQuery loadsAllData]) {
+ // We should have a tag for this
+ NSNumber *removedQueryTag = self.queryToTagMap[removedQuery];
+ [self.queryToTagMap removeObjectForKey:removedQuery];
+ [self.tagToQueryMap removeObjectForKey:removedQueryTag];
+ }
+ }];
+}
+
+- (FQuerySpec *) queryForListening:(FQuerySpec *)query {
+ if (query.loadsAllData && !query.isDefault) {
+ // We treat queries that load all data as default queries
+ return [FQuerySpec defaultQueryAtPath:query.path];
+ } else {
+ return query;
+ }
+}
+
+/**
+* For a given new listen, manage the de-duplication of outstanding subscriptions.
+* @return NSArray of FEvent events to support synchronous data sources
+*/
+- (NSArray *) setupListenerOnQuery:(FQuerySpec *)query view:(FView *)view {
+ FPath *path = query.path;
+ NSNumber *tagId = [self tagForQuery:query];
+ FListenContainer *listenContainer = [self createListenerForView:view];
+
+ NSArray *events = self.listenProvider.startListening([self queryForListening:query], tagId, listenContainer,
+ listenContainer.onComplete);
+
+ FImmutableTree *subtree = [self.syncPointTree subtreeAtPath:path];
+ // The root of this subtree has our query. We're here because we definitely need to send a listen for that, but we
+ // may need to shadow other listens as well.
+ if (tagId != nil) {
+ NSAssert(![subtree.value hasCompleteView], @"If we're adding a query, it shouldn't be shadowed");
+ } else {
+ // Shadow everything at or below this location, this is a default listener.
+ NSArray *queriesToStop = [subtree foldWithBlock:^id(FPath *relativePath, FSyncPoint *maybeChildSyncPoint, NSDictionary *childMap) {
+ if (![relativePath isEmpty] && maybeChildSyncPoint != nil && [maybeChildSyncPoint hasCompleteView]) {
+ return @[[maybeChildSyncPoint completeView].query];
+ } else {
+ // No default listener here, flatten any deeper queries into an array
+ NSMutableArray *queries = [[NSMutableArray alloc] init];
+ if (maybeChildSyncPoint != nil) {
+ for (FView *view in [maybeChildSyncPoint queryViews]) {
+ [queries addObject:view.query];
+ }
+ }
+ [childMap enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSArray *childQueries, BOOL *stop) {
+ [queries addObjectsFromArray:childQueries];
+ }];
+ return queries;
+ }
+ }];
+ for (FQuerySpec *queryToStop in queriesToStop) {
+ self.listenProvider.stopListening([self queryForListening:queryToStop], [self tagForQuery:queryToStop]);
+ }
+ }
+ return events;
+}
+
+- (FListenContainer *) createListenerForView:(FView *)view {
+ FQuerySpec *query = view.query;
+ NSNumber *tagId = [self tagForQuery:query];
+
+ FListenContainer *listenContainer = [[FListenContainer alloc] initWithView:view
+ onComplete:^(NSString *status) {
+ if ([status isEqualToString:@"ok"]) {
+ if (tagId != nil) {
+ return [self applyTaggedListenCompleteAtPath:query.path tagId:tagId];
+ } else {
+ return [self applyListenCompleteAtPath:query.path];
+ }
+ } else {
+ // If a listen failed, kill all of the listeners here, not just the one that triggered the error.
+ // Note that this may need to be scoped to just this listener if we change permissions on filtered children
+ NSError *error = [FUtilities errorForStatus:status andReason:nil];
+ FFWarn(@"I-RDB038012", @"Listener at %@ failed: %@", query.path, status);
+ return [self removeEventRegistration:nil forQuery:query cancelError:error];
+ }
+ }];
+
+ return listenContainer;
+}
+
+/**
+* @return The query associated with the given tag, if we have one
+*/
+- (FQuerySpec *) queryForTag:(NSNumber *)tagId {
+ return self.tagToQueryMap[tagId];
+}
+
+/**
+* @return The tag associated with the given query
+*/
+- (NSNumber *) tagForQuery:(FQuerySpec *)query {
+ return self.queryToTagMap[query];
+}
+
+#pragma mark -
+#pragma mark applyOperation Helpers
+
+/**
+* A helper method that visits all descendant and ancestor SyncPoints, applying the operation.
+*
+* NOTES:
+* - Descendant SyncPoints will be visited first (since we raise events depth-first).
+
+* - We call applyOperation: on each SyncPoint passing three things:
+* 1. A version of the Operation that has been made relative to the SyncPoint location.
+* 2. A WriteTreeRef of any writes we have cached at the SyncPoint location.
+* 3. A snapshot Node with cached server data, if we have it.
+
+* - We concatenate all of the events returned by each SyncPoint and return the result.
+*
+* @return Array of FEvent
+*/
+- (NSArray *) applyOperationToSyncPoints:(id<FOperation>)operation {
+ return [self applyOperationHelper:operation syncPointTree:self.syncPointTree serverCache:nil
+ writesCache:[self.pendingWriteTree childWritesForPath:[FPath empty]]];
+}
+
+/**
+* Recursive helper for applyOperationToSyncPoints_
+*/
+- (NSArray *) applyOperationHelper:(id<FOperation>)operation syncPointTree:(FImmutableTree *)syncPointTree
+ serverCache:(id<FNode>)serverCache writesCache:(FWriteTreeRef *)writesCache {
+ if ([operation.path isEmpty]) {
+ return [self applyOperationDescendantsHelper:operation syncPointTree:syncPointTree serverCache:serverCache writesCache:writesCache];
+ } else {
+ FSyncPoint *syncPoint = syncPointTree.value;
+
+ // If we don't have cached server data, see if we can get it from this SyncPoint
+ if (serverCache == nil && syncPoint != nil) {
+ serverCache = [syncPoint completeServerCacheAtPath:[FPath empty]];
+ }
+
+ NSMutableArray *events = [[NSMutableArray alloc] init];
+ NSString *childKey = [operation.path getFront];
+ id<FOperation> childOperation = [operation operationForChild:childKey];
+ FImmutableTree *childTree = [syncPointTree.children get:childKey];
+ if (childTree != nil && childOperation != nil) {
+ id<FNode> childServerCache = serverCache ? [serverCache getImmediateChild:childKey] : nil;
+ FWriteTreeRef *childWritesCache = [writesCache childWriteTreeRef:childKey];
+ [events addObjectsFromArray:[self applyOperationHelper:childOperation syncPointTree:childTree serverCache:childServerCache writesCache:childWritesCache]];
+ }
+
+ if (syncPoint) {
+ [events addObjectsFromArray:[syncPoint applyOperation:operation writesCache:writesCache serverCache:serverCache]];
+ }
+
+ return events;
+ }
+}
+
+/**
+* Recursive helper for applyOperationToSyncPoints:
+*/
+- (NSArray *) applyOperationDescendantsHelper:(id<FOperation>)operation syncPointTree:(FImmutableTree *)syncPointTree
+ serverCache:(id<FNode>)serverCache writesCache:(FWriteTreeRef *)writesCache {
+ FSyncPoint *syncPoint = syncPointTree.value;
+
+ // If we don't have cached server data, see if we can get it from this SyncPoint
+ id<FNode> resolvedServerCache;
+ if (serverCache == nil & syncPoint != nil) {
+ resolvedServerCache = [syncPoint completeServerCacheAtPath:[FPath empty]];
+ } else {
+ resolvedServerCache = serverCache;
+ }
+
+ NSMutableArray *events = [[NSMutableArray alloc] init];
+ [syncPointTree.children enumerateKeysAndObjectsUsingBlock:^(NSString *childKey, FImmutableTree *childTree, BOOL *stop) {
+ id<FNode> childServerCache = nil;
+ if (resolvedServerCache != nil) {
+ childServerCache = [resolvedServerCache getImmediateChild:childKey];
+ }
+ FWriteTreeRef *childWritesCache = [writesCache childWriteTreeRef:childKey];
+ id<FOperation> childOperation = [operation operationForChild:childKey];
+ if (childOperation != nil) {
+ [events addObjectsFromArray:[self applyOperationDescendantsHelper:childOperation
+ syncPointTree:childTree
+ serverCache:childServerCache
+ writesCache:childWritesCache]];
+ }
+ }];
+
+ if (syncPoint) {
+ [events addObjectsFromArray:[syncPoint applyOperation:operation writesCache:writesCache serverCache:resolvedServerCache]];
+ }
+
+ return events;
+}
+
+@end
diff --git a/Firebase/Database/Core/FWriteRecord.h b/Firebase/Database/Core/FWriteRecord.h
new file mode 100644
index 0000000..a9b53fe
--- /dev/null
+++ b/Firebase/Database/Core/FWriteRecord.h
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+@class FPath;
+@class FCompoundWrite;
+@protocol FNode;
+
+@interface FWriteRecord : NSObject
+
+- initWithPath:(FPath *)path overwrite:(id<FNode>)overwrite writeId:(NSInteger)writeId visible:(BOOL)isVisible;
+- initWithPath:(FPath *)path merge:(FCompoundWrite *)merge writeId:(NSInteger)writeId;
+
+@property (nonatomic, readonly) NSInteger writeId;
+@property (nonatomic, strong, readonly) FPath *path;
+@property (nonatomic, strong, readonly) id<FNode> overwrite;
+/**
+* Maps NSString -> id<FNode>
+*/
+@property (nonatomic, strong, readonly) FCompoundWrite *merge;
+@property (nonatomic, readonly) BOOL visible;
+
+- (BOOL)isMerge;
+- (BOOL)isOverwrite;
+
+@end
diff --git a/Firebase/Database/Core/FWriteRecord.m b/Firebase/Database/Core/FWriteRecord.m
new file mode 100644
index 0000000..47c952c
--- /dev/null
+++ b/Firebase/Database/Core/FWriteRecord.m
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FWriteRecord.h"
+#import "FPath.h"
+#import "FNode.h"
+#import "FCompoundWrite.h"
+
+@interface FWriteRecord ()
+@property (nonatomic, readwrite) NSInteger writeId;
+@property (nonatomic, strong, readwrite) FPath *path;
+@property (nonatomic, strong, readwrite) id<FNode> overwrite;
+@property (nonatomic, strong, readwrite) FCompoundWrite *merge;
+@property (nonatomic, readwrite) BOOL visible;
+@end
+
+@implementation FWriteRecord
+
+- (id)initWithPath:(FPath *)path overwrite:(id<FNode>)overwrite writeId:(NSInteger)writeId visible:(BOOL)isVisible {
+ self = [super init];
+ if (self) {
+ self.path = path;
+ if (overwrite == nil) {
+ [NSException raise:NSInvalidArgumentException format:@"Can't pass nil as overwrite parameter to an overwrite write record"];
+ }
+ self.overwrite = overwrite;
+ self.merge = nil;
+ self.writeId = writeId;
+ self.visible = isVisible;
+ }
+ return self;
+}
+
+- (id)initWithPath:(FPath *)path merge:(FCompoundWrite *)merge writeId:(NSInteger)writeId {
+ self = [super init];
+ if (self) {
+ self.path = path;
+ if (merge == nil) {
+ [NSException raise:NSInvalidArgumentException format:@"Can't pass nil as merge parameter to an merge write record"];
+ }
+ self.overwrite = nil;
+ self.merge = merge;
+ self.writeId = writeId;
+ self.visible = YES;
+ }
+ return self;
+}
+
+- (id<FNode>)overwrite {
+ if (self->_overwrite == nil) {
+ [NSException raise:NSInvalidArgumentException format:@"Can't get overwrite for merge write record!"];
+ }
+ return self->_overwrite;
+}
+
+- (FCompoundWrite *)compoundWrite {
+ if (self->_merge == nil) {
+ [NSException raise:NSInvalidArgumentException format:@"Can't get merge for overwrite write record!"];
+ }
+ return self->_merge;
+}
+
+- (BOOL)isMerge {
+ return self->_merge != nil;
+}
+
+- (BOOL)isOverwrite {
+ return self->_overwrite != nil;
+}
+
+- (NSString *)description {
+ if (self.isOverwrite) {
+ return [NSString stringWithFormat:@"FWriteRecord { writeId = %lu, path = %@, overwrite = %@, visible = %d }",
+ (unsigned long)self.writeId, self.path, self.overwrite, self.visible];
+ } else {
+ return [NSString stringWithFormat:@"FWriteRecord { writeId = %lu, path = %@, merge = %@ }",
+ (unsigned long)self.writeId, self.path, self.merge];
+ }
+}
+
+- (BOOL)isEqual:(id)object {
+ if (![object isKindOfClass:[self class]]) {
+ return NO;
+ }
+ FWriteRecord *other = (FWriteRecord *)object;
+ if (self->_writeId != other->_writeId) return NO;
+ if (self->_path != other->_path && ![self->_path isEqual:other->_path]) return NO;
+ if (self->_overwrite != other->_overwrite && ![self->_overwrite isEqual:other->_overwrite]) return NO;
+ if (self->_merge != other->_merge && ![self->_merge isEqual:other->_merge]) return NO;
+ if (self->_visible != other->_visible) return NO;
+
+ return YES;
+}
+
+- (NSUInteger)hash {
+ NSUInteger hash = self->_writeId * 17;
+ hash = hash * 31 + self->_path.hash;
+ hash = hash * 31 + self->_overwrite.hash;
+ hash = hash * 31 + self->_merge.hash;
+ hash = hash * 31 + ((self->_visible) ? 1 : 0);
+ return hash;
+}
+
+@end
diff --git a/Firebase/Database/Core/FWriteTree.h b/Firebase/Database/Core/FWriteTree.h
new file mode 100644
index 0000000..243bc9f
--- /dev/null
+++ b/Firebase/Database/Core/FWriteTree.h
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+@class FPath;
+@protocol FNode;
+@class FCompoundWrite;
+@class FWriteTreeRef;
+@class FChildrenNode;
+@class FNamedNode;
+@class FWriteRecord;
+@protocol FIndex;
+@class FCacheNode;
+
+@interface FWriteTree : NSObject
+
+- (FWriteTreeRef *) childWritesForPath:(FPath *)path;
+- (void) addOverwriteAtPath:(FPath *)path newData:(id<FNode>)newData writeId:(NSInteger)writeId isVisible:(BOOL)visible;
+- (void) addMergeAtPath:(FPath *)path changedChildren:(FCompoundWrite *)changedChildren writeId:(NSInteger)writeId;
+- (BOOL) removeWriteId:(NSInteger)writeId;
+- (NSArray *) removeAllWrites;
+- (FWriteRecord *)writeForId:(NSInteger)writeId;
+
+- (id<FNode>) calculateCompleteEventCacheAtPath:(FPath *)treePath
+ completeServerCache:(id<FNode>)completeServerCache
+ excludeWriteIds:(NSArray *)writeIdsToExclude
+ includeHiddenWrites:(BOOL)includeHiddenWrites;
+
+- (id<FNode>) calculateCompleteEventChildrenAtPath:(FPath *)treePath
+ completeServerChildren:(id<FNode>)completeServerChildren;
+
+- (id<FNode>) calculateEventCacheAfterServerOverwriteAtPath:(FPath *)treePath
+ childPath:(FPath *)childPath
+ existingEventSnap:(id<FNode>)existingEventSnap
+ existingServerSnap:(id<FNode>)existingServerSnap;
+
+- (id<FNode>) calculateCompleteChildAtPath:(FPath *)treePath
+ childKey:(NSString *)childKey
+ cache:(FCacheNode *)existingServerCache;
+
+- (id<FNode>) shadowingWriteAtPath:(FPath *)path;
+
+- (FNamedNode *) calculateNextNodeAfterPost:(FNamedNode *)post
+ atPath:(FPath *)path
+ completeServerData:(id<FNode>)completeServerData
+ reverse:(BOOL)reverse
+ index:(id<FIndex>)index;
+
+@end
diff --git a/Firebase/Database/Core/FWriteTree.m b/Firebase/Database/Core/FWriteTree.m
new file mode 100644
index 0000000..c5b08ea
--- /dev/null
+++ b/Firebase/Database/Core/FWriteTree.m
@@ -0,0 +1,458 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FWriteTree.h"
+#import "FImmutableTree.h"
+#import "FPath.h"
+#import "FNode.h"
+#import "FWriteTreeRef.h"
+#import "FChildrenNode.h"
+#import "FNamedNode.h"
+#import "FWriteRecord.h"
+#import "FEmptyNode.h"
+#import "FIndex.h"
+#import "FCompoundWrite.h"
+#import "FCacheNode.h"
+
+@interface FWriteTree ()
+/**
+* A tree tracking the results of applying all visible writes. This does not include transactions with
+* applyLocally=false or writes that are completely shadowed by other writes.
+* Contains id<FNode> as values.
+*/
+@property (nonatomic, strong) FCompoundWrite *visibleWrites;
+/**
+* A list of pending writes, regardless of visibility and shadowed-ness. Used to calcuate arbitrary
+* sets of the changed data, such as hidden writes (from transactions) or changes with certain writes excluded (also
+* used by transactions).
+* Contains FWriteRecords.
+*/
+@property (nonatomic, strong) NSMutableArray *allWrites;
+@property (nonatomic) NSInteger lastWriteId;
+@end
+
+/**
+* FWriteTree tracks all pending user-initiated writes and has methods to calcuate the result of merging them with
+* underlying server data (to create "event cache" data). Pending writes are added with addOverwriteAtPath: and
+* addMergeAtPath: and removed with removeWriteId:.
+*/
+@implementation FWriteTree
+
+@synthesize allWrites;
+@synthesize lastWriteId;
+
+- (id) init {
+ self = [super init];
+ if (self) {
+ self.visibleWrites = [FCompoundWrite emptyWrite];
+ self.allWrites = [[NSMutableArray alloc] init];
+ self.lastWriteId = -1;
+ }
+ return self;
+}
+
+/**
+* Create a new WriteTreeRef for the given path. For use with a new sync point at the given path.
+*/
+- (FWriteTreeRef *) childWritesForPath:(FPath *)path {
+ return [[FWriteTreeRef alloc] initWithPath:path writeTree:self];
+}
+
+/**
+* Record a new overwrite from user code.
+* @param visible Is set to false by some transactions. It should be excluded from event caches.
+*/
+- (void) addOverwriteAtPath:(FPath *)path newData:(id <FNode>)newData writeId:(NSInteger)writeId isVisible:(BOOL)visible {
+ NSAssert(writeId > self.lastWriteId, @"Stacking an older write on top of a newer one");
+ FWriteRecord *record = [[FWriteRecord alloc] initWithPath:path overwrite:newData writeId:writeId visible:visible];
+ [self.allWrites addObject:record];
+
+ if (visible) {
+ self.visibleWrites = [self.visibleWrites addWrite:newData atPath:path];
+ }
+
+ self.lastWriteId = writeId;
+}
+
+/**
+* Record a new merge from user code.
+* @param changedChildren maps NSString -> id<FNode>
+*/
+- (void) addMergeAtPath:(FPath *)path changedChildren:(FCompoundWrite *)changedChildren writeId:(NSInteger)writeId {
+ NSAssert(writeId > self.lastWriteId, @"Stacking an older merge on top of newer one");
+ FWriteRecord *record = [[FWriteRecord alloc] initWithPath:path merge:changedChildren writeId:writeId];
+ [self.allWrites addObject:record];
+
+ self.visibleWrites = [self.visibleWrites addCompoundWrite:changedChildren atPath:path];
+ self.lastWriteId = writeId;
+}
+
+- (FWriteRecord *)writeForId:(NSInteger)writeId {
+ NSUInteger index = [self.allWrites indexOfObjectPassingTest:^BOOL(FWriteRecord *write, NSUInteger idx, BOOL *stop) {
+ return write.writeId == writeId;
+ }];
+ return (index == NSNotFound) ? nil : self.allWrites[index];
+}
+
+/**
+* Remove a write (either an overwrite or merge) that has been successfully acknowledged by the server. Recalculates the
+* tree if necessary. We return the path of the write and whether it may have been visible, meaning views need to
+* reevaluate.
+*
+* @return YES if the write may have been visible (meaning we'll need to reevaluate / raise events as a result).
+*/
+- (BOOL) removeWriteId:(NSInteger)writeId {
+ NSUInteger index = [self.allWrites indexOfObjectPassingTest:^BOOL(FWriteRecord *record, NSUInteger idx, BOOL *stop) {
+ if (record.writeId == writeId) {
+ return YES;
+ } else {
+ return NO;
+ }
+ }];
+ NSAssert(index != NSNotFound, @"[FWriteTree removeWriteId:] called with nonexistent writeId.");
+ FWriteRecord *writeToRemove = self.allWrites[index];
+ [self.allWrites removeObjectAtIndex:index];
+
+ BOOL removedWriteWasVisible = writeToRemove.visible;
+ BOOL removedWriteOverlapsWithOtherWrites = NO;
+ NSInteger i = [self.allWrites count] - 1;
+
+ while (removedWriteWasVisible && i >= 0) {
+ FWriteRecord *currentWrite = [self.allWrites objectAtIndex:i];
+ if (currentWrite.visible) {
+ if (i >= index && [self record:currentWrite containsPath:writeToRemove.path]) {
+ // The removed write was completely shadowed by a subsequent write.
+ removedWriteWasVisible = NO;
+ } else if ([writeToRemove.path contains:currentWrite.path]) {
+ // Either we're covering some writes or they're covering part of us (depending on which came first).
+ removedWriteOverlapsWithOtherWrites = YES;
+ }
+ }
+ i--;
+ }
+
+ if (!removedWriteWasVisible) {
+ return NO;
+ } else if (removedWriteOverlapsWithOtherWrites) {
+ // There's some shadowing going on. Just rebuild the visible writes from scratch.
+ [self resetTree];
+ return YES;
+ } else {
+ // There's no shadowing. We can safely just remove the write(s) from visibleWrites.
+ if ([writeToRemove isOverwrite]) {
+ self.visibleWrites = [self.visibleWrites removeWriteAtPath:writeToRemove.path];
+ } else {
+ FCompoundWrite *merge = writeToRemove.merge;
+ [merge enumerateWrites:^(FPath *path, id<FNode> node, BOOL *stop) {
+ self.visibleWrites = [self.visibleWrites removeWriteAtPath:[writeToRemove.path child:path]];
+ }];
+ }
+ return YES;
+ }
+}
+
+- (NSArray *) removeAllWrites {
+ NSArray *writes = self.allWrites;
+ self.visibleWrites = [FCompoundWrite emptyWrite];
+ self.allWrites = [NSMutableArray array];
+ return writes;
+}
+
+/**
+* @return A complete snapshot for the given path if there's visible write data at that path, else nil.
+* No server data is considered.
+*/
+- (id <FNode>) completeWriteDataAtPath:(FPath *)path {
+ return [self.visibleWrites completeNodeAtPath:path];
+}
+
+/**
+* Given optional, underlying server data, and an optional set of constraints (exclude some sets, include hidden
+* writes), attempt to calculate a complete snapshot for the given path
+* @param includeHiddenWrites Defaults to false, whether or not to layer on writes with visible set to false
+*/
+- (id <FNode>) calculateCompleteEventCacheAtPath:(FPath *)treePath completeServerCache:(id <FNode>)completeServerCache
+ excludeWriteIds:(NSArray *)writeIdsToExclude includeHiddenWrites:(BOOL)includeHiddenWrites {
+ if (writeIdsToExclude == nil && !includeHiddenWrites) {
+ id<FNode> shadowingNode = [self.visibleWrites completeNodeAtPath:treePath];
+ if (shadowingNode != nil) {
+ return shadowingNode;
+ } else {
+ // No cache here. Can't claim complete knowledge.
+ FCompoundWrite *subMerge = [self.visibleWrites childCompoundWriteAtPath:treePath];
+ if (subMerge.isEmpty) {
+ return completeServerCache;
+ } else if (completeServerCache == nil && ![subMerge hasCompleteWriteAtPath:[FPath empty]]) {
+ // We wouldn't have a complete snapshot since there's no underlying data and no complete shadow
+ return nil;
+ } else {
+ id<FNode> layeredCache = completeServerCache != nil ? completeServerCache : [FEmptyNode emptyNode];
+ return [subMerge applyToNode:layeredCache];
+ }
+ }
+ } else {
+ FCompoundWrite *merge = [self.visibleWrites childCompoundWriteAtPath:treePath];
+ if (!includeHiddenWrites && merge.isEmpty) {
+ return completeServerCache;
+ } else {
+ // If the server cache is null and we don't have a complete cache, we need to return nil
+ if (!includeHiddenWrites && completeServerCache == nil && ![merge hasCompleteWriteAtPath:[FPath empty]]) {
+ return nil;
+ } else {
+ BOOL (^filter) (FWriteRecord *) = ^(FWriteRecord *record) {
+ return (BOOL) ((record.visible || includeHiddenWrites) &&
+ (writeIdsToExclude == nil || ![writeIdsToExclude containsObject:[NSNumber numberWithInteger:record.writeId]]) &&
+ ([record.path contains:treePath] || [treePath contains:record.path]));
+ };
+ FCompoundWrite *mergeAtPath = [FWriteTree layerTreeFromWrites:self.allWrites filter:filter treeRoot:treePath];
+ id<FNode> layeredCache = completeServerCache ? completeServerCache : [FEmptyNode emptyNode];
+ return [mergeAtPath applyToNode:layeredCache];
+ }
+ }
+ }
+}
+
+/**
+* With optional, underlying server data, attempt to return a children node of children that we have complete data for.
+* Used when creating new views, to pre-fill their complete event children snapshot.
+*/
+- (FChildrenNode *) calculateCompleteEventChildrenAtPath:(FPath *)treePath
+ completeServerChildren:(id<FNode>)completeServerChildren {
+ __block id<FNode> completeChildren = [FEmptyNode emptyNode];
+ id<FNode> topLevelSet = [self.visibleWrites completeNodeAtPath:treePath];
+ if (topLevelSet != nil) {
+ if (![topLevelSet isLeafNode]) {
+ // We're shadowing everything. Return the children.
+ FChildrenNode *topChildrenNode = topLevelSet;
+ [topChildrenNode enumerateChildrenUsingBlock:^(NSString *key, id<FNode> node, BOOL *stop) {
+ completeChildren = [completeChildren updateImmediateChild:key withNewChild:node];
+ }];
+ }
+ return completeChildren;
+ } else {
+ // Layer any children we have on top of this
+ // We know we don't have a top-level set, so just enumerate existing children, and apply any updates
+ FCompoundWrite *merge = [self.visibleWrites childCompoundWriteAtPath:treePath];
+ [completeServerChildren enumerateChildrenUsingBlock:^(NSString *key, id<FNode> node, BOOL *stop) {
+ FCompoundWrite *childMerge = [merge childCompoundWriteAtPath:[[FPath alloc] initWith:key]];
+ id<FNode> newChildNode = [childMerge applyToNode:node];
+ completeChildren = [completeChildren updateImmediateChild:key withNewChild:newChildNode];
+ }];
+ // Add any complete children we have from the set.
+ for (FNamedNode *node in merge.completeChildren) {
+ completeChildren = [completeChildren updateImmediateChild:node.name withNewChild:node.node];
+ }
+ return completeChildren;
+ }
+}
+
+/**
+* Given that the underlying server data has updated, determine what, if anything, needs to be applied to the event cache.
+*
+* Possibilities
+*
+* 1. No write are shadowing. Events should be raised, the snap to be applied comes from the server data.
+*
+* 2. Some write is completely shadowing. No events to be raised.
+*
+* 3. Is partially shadowed. Events ..
+*
+* Either existingEventSnap or existingServerSnap must exist.
+*/
+- (id <FNode>) calculateEventCacheAfterServerOverwriteAtPath:(FPath *)treePath childPath:(FPath *)childPath existingEventSnap:(id <FNode>)existingEventSnap existingServerSnap:(id <FNode>)existingServerSnap {
+ NSAssert(existingEventSnap != nil || existingServerSnap != nil,
+ @"Either existingEventSnap or existingServerSanp must exist.");
+
+ FPath *path = [treePath child:childPath];
+ if ([self.visibleWrites hasCompleteWriteAtPath:path]) {
+ // At this point we can probably guarantee that we're in case 2, meaning no events
+ // May need to check visibility while doing the findRootMostValueAndPath call
+ return nil;
+ } else {
+ // This could be more efficient if the serverNode + updates doesn't change the eventSnap
+ // However this is tricky to find out, since user updates don't necessary change the server
+ // snap, e.g. priority updates on empty nodes, or deep deletes. Another special case is if the server
+ // adds nodes, but doesn't change any existing writes. It is therefore not enough to
+ // only check if the updates change the serverNode.
+ // Maybe check if the merge tree contains these special cases and only do a full overwrite in that case?
+ FCompoundWrite *childMerge = [self.visibleWrites childCompoundWriteAtPath:path];
+ if (childMerge.isEmpty) {
+ // We're not shadowing at all. Case 1
+ return [existingServerSnap getChild:childPath];
+ } else {
+ return [childMerge applyToNode:[existingServerSnap getChild:childPath]];
+ }
+ }
+}
+
+/**
+* Returns a complete child for a given server snap after applying all user writes or nil if there is no complete child
+* for this child key.
+*/
+- (id<FNode>) calculateCompleteChildAtPath:(FPath *)treePath childKey:(NSString *)childKey cache:(FCacheNode *)existingServerCache {
+ FPath *path = [treePath childFromString:childKey];
+ id<FNode> shadowingNode = [self.visibleWrites completeNodeAtPath:path];
+ if (shadowingNode != nil) {
+ return shadowingNode;
+ } else {
+ if ([existingServerCache isCompleteForChild:childKey]) {
+ FCompoundWrite *childMerge = [self.visibleWrites childCompoundWriteAtPath:path];
+ return [childMerge applyToNode:[existingServerCache.node getImmediateChild:childKey]];
+ } else {
+ return nil;
+ }
+ }
+}
+
+/**
+* Returns a node if there is a complete overwrite for this path. More specifically, if there is a write at
+* a higher path, this will return the child of that write relative to the write and this path.
+* Returns null if there is no write at this path.
+*/
+- (id<FNode>) shadowingWriteAtPath:(FPath *)path {
+ return [self.visibleWrites completeNodeAtPath:path];
+}
+
+/**
+* This method is used when processing child remove events on a query. If we can, we pull in children that were outside
+* the window, but may now be in the window.
+*/
+- (FNamedNode *)calculateNextNodeAfterPost:(FNamedNode *)post
+ atPath:(FPath *)treePath
+ completeServerData:(id<FNode>)completeServerData
+ reverse:(BOOL)reverse
+ index:(id<FIndex>)index
+{
+ __block id<FNode> toIterate;
+ FCompoundWrite *merge = [self.visibleWrites childCompoundWriteAtPath:treePath];
+ id<FNode> shadowingNode = [merge completeNodeAtPath:[FPath empty]];
+ if (shadowingNode != nil) {
+ toIterate = shadowingNode;
+ } else if (completeServerData != nil) {
+ toIterate = [merge applyToNode:completeServerData];
+ } else {
+ return nil;
+ }
+
+ __block NSString *currentNextKey = nil;
+ __block id<FNode> currentNextNode = nil;
+ [toIterate enumerateChildrenUsingBlock:^(NSString *key, id<FNode> node, BOOL *stop) {
+ if ([index compareKey:key andNode:node toOtherKey:post.name andNode:post.node reverse:reverse] > NSOrderedSame &&
+ (!currentNextKey || [index compareKey:key andNode:node toOtherKey:currentNextKey andNode:currentNextNode reverse:reverse] < NSOrderedSame)) {
+ currentNextKey = key;
+ currentNextNode = node;
+ }
+ }];
+
+ if (currentNextKey != nil) {
+ return [FNamedNode nodeWithName:currentNextKey node:currentNextNode];
+ } else {
+ return nil;
+ }
+}
+
+#pragma mark -
+#pragma mark Private Methods
+
+- (BOOL) record:(FWriteRecord *)record containsPath:(FPath *)path {
+ if ([record isOverwrite]) {
+ return [record.path contains:path];
+ } else {
+ __block BOOL contains = NO;
+ [record.merge enumerateWrites:^(FPath *childPath, id<FNode> node, BOOL *stop) {
+ contains = [[record.path child:childPath] contains:path];
+ *stop = contains;
+ }];
+ return contains;
+ }
+}
+
+/**
+* Re-layer the writes and merges into a tree so we can efficiently calculate event snapshots
+*/
+- (void) resetTree {
+ self.visibleWrites = [FWriteTree layerTreeFromWrites:self.allWrites filter:[FWriteTree defaultFilter] treeRoot:[FPath empty]];
+ if ([self.allWrites count] > 0) {
+ FWriteRecord *lastRecord = self.allWrites[[self.allWrites count] - 1];
+ self.lastWriteId = lastRecord.writeId;
+ } else {
+ self.lastWriteId = -1;
+ }
+}
+
+/**
+* The default filter used when constructing the tree. Keep everything that's visible.
+*/
++ (BOOL (^)(FWriteRecord *record)) defaultFilter {
+ static BOOL (^filter)(FWriteRecord *);
+ static dispatch_once_t filterToken;
+ dispatch_once(&filterToken, ^{
+ filter = ^(FWriteRecord *record) {
+ return YES;
+ };
+ });
+ return filter;
+}
+
+/**
+* Static method. Given an array of WriteRecords, a filter for which ones to include, and a path, construct a merge
+* at that path
+* @return An FImmutableTree of id<FNode>s.
+*/
++ (FCompoundWrite *) layerTreeFromWrites:(NSArray *)writes filter:(BOOL (^)(FWriteRecord *record))filter treeRoot:(FPath *)treeRoot {
+ __block FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite];
+ [writes enumerateObjectsUsingBlock:^(FWriteRecord *record, NSUInteger idx, BOOL *stop) {
+ // Theory, a later set will either:
+ // a) abort a relevant transaction, so no need to worry about excluding it from calculating that transaction
+ // b) not be relevant to a transaction (separate branch), so again will not affect the data for that transaction
+ if (filter(record)) {
+ FPath *writePath = record.path;
+ if ([record isOverwrite]) {
+ if ([treeRoot contains:writePath]) {
+ FPath *relativePath = [FPath relativePathFrom:treeRoot to:writePath];
+ compoundWrite = [compoundWrite addWrite:record.overwrite atPath:relativePath];
+ } else if ([writePath contains:treeRoot]) {
+ id<FNode> child = [record.overwrite getChild:[FPath relativePathFrom:writePath to:treeRoot]];
+ compoundWrite = [compoundWrite addWrite:child atPath:[FPath empty]];
+ } else {
+ // There is no overlap between root path and write path, ignore write
+ }
+ } else {
+ if ([treeRoot contains:writePath]) {
+ FPath *relativePath = [FPath relativePathFrom:treeRoot to:writePath];
+ compoundWrite = [compoundWrite addCompoundWrite:record.merge atPath:relativePath];
+ } else if ([writePath contains:treeRoot]) {
+ FPath *relativePath = [FPath relativePathFrom:writePath to:treeRoot];
+ if (relativePath.isEmpty) {
+ compoundWrite = [compoundWrite addCompoundWrite:record.merge atPath:[FPath empty]];
+ } else {
+ id<FNode> child = [record.merge completeNodeAtPath:relativePath];
+ if (child != nil) {
+ // There exists a child in this node that matches the root path
+ id<FNode> deepNode = [child getChild:[relativePath popFront]];
+ compoundWrite = [compoundWrite addWrite:deepNode atPath:[FPath empty]];
+ }
+ }
+ } else {
+ // There is no overlap between root path and write path, ignore write
+ }
+ }
+ }
+ }];
+ return compoundWrite;
+}
+
+@end
diff --git a/Firebase/Database/Core/FWriteTreeRef.h b/Firebase/Database/Core/FWriteTreeRef.h
new file mode 100644
index 0000000..791dd26
--- /dev/null
+++ b/Firebase/Database/Core/FWriteTreeRef.h
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+@protocol FNode;
+@class FChildrenNode;
+@class FPath;
+@class FNamedNode;
+@class FWriteRecord;
+@class FWriteTree;
+@protocol FIndex;
+@class FCacheNode;
+
+@interface FWriteTreeRef : NSObject
+
+- (id) initWithPath:(FPath *)aPath writeTree:(FWriteTree *)tree;
+
+- (id <FNode>) calculateCompleteEventCacheWithCompleteServerCache:(id <FNode>)completeServerCache;
+
+- (FChildrenNode *) calculateCompleteEventChildrenWithCompleteServerChildren:(FChildrenNode *)completeServerChildren;
+
+- (id<FNode>) calculateEventCacheAfterServerOverwriteWithChildPath:(FPath *)childPath
+ existingEventSnap:(id<FNode>)existingEventSnap
+ existingServerSnap:(id<FNode>)existingServerSnap;
+
+- (id<FNode>) shadowingWriteAtPath:(FPath *)path;
+
+- (FNamedNode *) calculateNextNodeAfterPost:(FNamedNode *)post
+ completeServerData:(id<FNode>)completeServerData
+ reverse:(BOOL)reverse
+ index:(id<FIndex>)index;
+
+- (id<FNode>) calculateCompleteChild:(NSString *)childKey cache:(FCacheNode *)existingServerCache;
+
+- (FWriteTreeRef *) childWriteTreeRef:(NSString *)childKey;
+
+@end
diff --git a/Firebase/Database/Core/FWriteTreeRef.m b/Firebase/Database/Core/FWriteTreeRef.m
new file mode 100644
index 0000000..392369b
--- /dev/null
+++ b/Firebase/Database/Core/FWriteTreeRef.m
@@ -0,0 +1,133 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FWriteTreeRef.h"
+#import "FPath.h"
+#import "FNode.h"
+#import "FWriteTree.h"
+#import "FChildrenNode.h"
+#import "FNamedNode.h"
+#import "FWriteRecord.h"
+#import "FIndex.h"
+#import "FCacheNode.h"
+
+@interface FWriteTreeRef ()
+/**
+* The path to this particular FWriteTreeRef. Used for calling methods on writeTree while exposing a simpler interface
+* to callers.
+*/
+@property (nonatomic, strong) FPath *path;
+/**
+* A reference to the actual tree of the write data. All methods are pass-through to the tree, but with the appropriate
+* path prefixed.
+*
+* This lets us make cheap references to points in the tree for sync points without having to copy and maintain all of
+* the data.
+*/
+@property (nonatomic, strong) FWriteTree *writeTree;
+@end
+
+/**
+* A FWriteTreeRef wraps a FWriteTree and a FPath, for convenient access to a particular subtree. All the methods just
+* proxy to the underlying FWriteTree.
+*/
+@implementation FWriteTreeRef
+- (id) initWithPath:(FPath *)aPath writeTree:(FWriteTree *)tree {
+ self = [super init];
+ if (self) {
+ self.path = aPath;
+ self.writeTree = tree;
+ }
+ return self;
+}
+
+/**
+* @return If possible, returns a complete event cache, using the underlying server data if possible. In addition, can
+* be used to get a cache that includes hidden writes, and excludes arbitrary writes. Note that customizing the returned
+* node can lead to a more expensive calculation.
+*/
+- (id <FNode>) calculateCompleteEventCacheWithCompleteServerCache:(id<FNode>)completeServerCache {
+ return [self.writeTree calculateCompleteEventCacheAtPath:self.path completeServerCache:completeServerCache excludeWriteIds:nil includeHiddenWrites:NO];
+}
+
+/**
+* @return If possible, returns a children node containing all of the complete children we have data for. The returned
+* data is a mix of the given server data and write data.
+*/
+- (FChildrenNode *) calculateCompleteEventChildrenWithCompleteServerChildren:(id<FNode>)completeServerChildren {
+ return [self.writeTree calculateCompleteEventChildrenAtPath:self.path completeServerChildren:completeServerChildren];
+}
+
+/**
+* Given that either the underlying server data has updated or the outstanding writes have been updating, determine what,
+* if anything, needs to be applied to the event cache.
+*
+* Possibilities:
+*
+* 1. No writes are shadowing. Events should be raised, the snap to be applied comes from the server data.
+*
+* 2. Some writes are completly shadowing. No events to be raised.
+*
+* 3. Is partially shadowed. Events should be raised.
+*
+* Either existingEventSnap or existingServerSnap must exist, this is validated via an assert.
+*/
+- (id<FNode>) calculateEventCacheAfterServerOverwriteWithChildPath:(FPath *)childPath existingEventSnap:(id <FNode>)existingEventSnap existingServerSnap:(id <FNode>)existingServerSnap {
+ return [self.writeTree calculateEventCacheAfterServerOverwriteAtPath:self.path childPath:childPath existingEventSnap:existingEventSnap existingServerSnap:existingServerSnap];
+}
+
+/**
+* Returns a node if there is a complete overwrite for this path. More specifically, if there is a write at a higher
+* path, this will return the child of that write relative to the write and this path.
+* Returns nil if there is no write at this path.
+*/
+- (id<FNode>) shadowingWriteAtPath:(FPath *)path {
+ return [self.writeTree shadowingWriteAtPath:[self.path child:path]];
+}
+
+/**
+* This method is used when processing child remove events on a query. If we can, we pull in children that are outside
+* the window, but may now be in the window.
+*/
+- (FNamedNode *)calculateNextNodeAfterPost:(FNamedNode *)post
+ completeServerData:(id<FNode>)completeServerData
+ reverse:(BOOL)reverse
+ index:(id<FIndex>)index
+{
+ return [self.writeTree calculateNextNodeAfterPost:post
+ atPath:self.path
+ completeServerData:completeServerData
+ reverse:reverse
+ index:index];
+}
+
+/**
+* Returns a complete child for a given server snap after applying all user writes or nil if there is no complete child
+* for this child key.
+*/
+- (id<FNode>) calculateCompleteChild:(NSString *)childKey cache:(FCacheNode *)existingServerCache {
+ return [self.writeTree calculateCompleteChildAtPath:self.path childKey:childKey cache:existingServerCache];
+}
+
+/**
+* @return a WriteTreeref for a child.
+*/
+- (FWriteTreeRef *) childWriteTreeRef:(NSString *)childKey {
+ return [[FWriteTreeRef alloc] initWithPath:[self.path childFromString:childKey] writeTree:self.writeTree];
+}
+
+
+@end
diff --git a/Firebase/Database/Core/Operation/FAckUserWrite.h b/Firebase/Database/Core/Operation/FAckUserWrite.h
new file mode 100644
index 0000000..a337996
--- /dev/null
+++ b/Firebase/Database/Core/Operation/FAckUserWrite.h
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FOperation.h"
+
+@class FPath;
+@class FOperationSource;
+@class FImmutableTree;
+
+
+@interface FAckUserWrite : NSObject <FOperation>
+
+- initWithPath:(FPath *)operationPath affectedTree:(FImmutableTree *)affectedTree revert:(BOOL)shouldRevert;
+
+@property (nonatomic, strong, readonly) FOperationSource *source;
+@property (nonatomic, readonly) FOperationType type;
+@property (nonatomic, strong, readonly) FPath *path;
+// A FImmutableTree, containing @YES for each affected path. Affected paths can't overlap.
+@property (nonatomic, strong, readonly) FImmutableTree *affectedTree;
+@property (nonatomic, readonly) BOOL revert;
+
+@end
diff --git a/Firebase/Database/Core/Operation/FAckUserWrite.m b/Firebase/Database/Core/Operation/FAckUserWrite.m
new file mode 100644
index 0000000..f81e7f5
--- /dev/null
+++ b/Firebase/Database/Core/Operation/FAckUserWrite.m
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FAckUserWrite.h"
+#import "FPath.h"
+#import "FOperationSource.h"
+#import "FImmutableTree.h"
+
+
+@implementation FAckUserWrite
+
+- (id) initWithPath:(FPath *)operationPath affectedTree:(FImmutableTree *)tree revert:(BOOL)shouldRevert {
+ self = [super init];
+ if (self) {
+ self->_source = [FOperationSource userInstance];
+ self->_type = FOperationTypeAckUserWrite;
+ self->_path = operationPath;
+ self->_affectedTree = tree;
+ self->_revert = shouldRevert;
+ }
+ return self;
+}
+
+- (FAckUserWrite *) operationForChild:(NSString *)childKey {
+ if (![self.path isEmpty]) {
+ NSAssert([self.path.getFront isEqualToString:childKey], @"operationForChild called for unrelated child.");
+ return [[FAckUserWrite alloc] initWithPath:[self.path popFront] affectedTree:self.affectedTree revert:self.revert];
+ } else if (self.affectedTree.value != nil) {
+ NSAssert(self.affectedTree.children.isEmpty, @"affectedTree should not have overlapping affected paths.");
+ // All child locations are affected as well; just return same operation.
+ return self;
+ } else {
+ FImmutableTree *childTree = [self.affectedTree subtreeAtPath:[[FPath alloc] initWith:childKey]];
+ return [[FAckUserWrite alloc] initWithPath:[FPath empty] affectedTree:childTree revert:self.revert];
+ }
+}
+
+- (NSString *) description {
+ return [NSString stringWithFormat:@"FAckUserWrite { path=%@, revert=%d, affectedTree=%@ }", self.path, self.revert, self.affectedTree];
+}
+
+@end
diff --git a/Firebase/Database/Core/Operation/FMerge.h b/Firebase/Database/Core/Operation/FMerge.h
new file mode 100644
index 0000000..4cab613
--- /dev/null
+++ b/Firebase/Database/Core/Operation/FMerge.h
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FOperation.h"
+
+@class FCompoundWrite;
+
+@interface FMerge : NSObject <FOperation>
+
+- (id) initWithSource:(FOperationSource *)aSource path:(FPath *)aPath children:(FCompoundWrite *)children;
+
+@property (nonatomic, strong, readonly) FOperationSource *source;
+@property (nonatomic, readonly) FOperationType type;
+@property (nonatomic, strong, readonly) FPath *path;
+@property (nonatomic, strong, readonly) FCompoundWrite *children;
+
+@end
diff --git a/Firebase/Database/Core/Operation/FMerge.m b/Firebase/Database/Core/Operation/FMerge.m
new file mode 100644
index 0000000..8e6d924
--- /dev/null
+++ b/Firebase/Database/Core/Operation/FMerge.m
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FMerge.h"
+#import "FOperationSource.h"
+#import "FPath.h"
+#import "FNode.h"
+#import "FOverwrite.h"
+#import "FCompoundWrite.h"
+
+@interface FMerge ()
+@property (nonatomic, strong, readwrite) FOperationSource *source;
+@property (nonatomic, readwrite) FOperationType type;
+@property (nonatomic, strong, readwrite) FPath *path;
+@property (nonatomic, strong) FCompoundWrite *children;
+@end
+
+@implementation FMerge
+
+@synthesize source;
+@synthesize type;
+@synthesize path;
+@synthesize children;
+
+- (id) initWithSource:(FOperationSource *)aSource path:(FPath *)aPath children:(FCompoundWrite *)someChildren {
+ self = [super init];
+ if (self) {
+ self.source = aSource;
+ self.type = FOperationTypeMerge;
+ self.path = aPath;
+ self.children = someChildren;
+ }
+ return self;
+}
+
+- (id<FOperation>) operationForChild:(NSString *)childKey {
+ if ([self.path isEmpty]) {
+ FCompoundWrite *childTree = [self.children childCompoundWriteAtPath:[[FPath alloc] initWith:childKey]];
+ if (childTree.isEmpty) {
+ return nil;
+ } else if (childTree.rootWrite != nil) {
+ // We have a snapshot for the child in question. This becomes an overwrite of the child.
+ return [[FOverwrite alloc] initWithSource:self.source path:[FPath empty] snap:childTree.rootWrite];
+ } else {
+ // This is a merge at a deeper level
+ return [[FMerge alloc] initWithSource:self.source path:[FPath empty] children:childTree];
+ }
+ } else {
+ NSAssert([self.path.getFront isEqualToString:childKey], @"Can't get a merge for a child not on the path of the operation");
+ return [[FMerge alloc] initWithSource:self.source path:[self.path popFront] children:self.children];
+ }
+}
+
+- (NSString *) description {
+ return [NSString stringWithFormat:@"FMerge { path=%@, soruce=%@ children=%@}", self.path, self.source, self.children];
+}
+
+@end
diff --git a/Firebase/Database/Core/Operation/FOperation.h b/Firebase/Database/Core/Operation/FOperation.h
new file mode 100644
index 0000000..2bbbbd2
--- /dev/null
+++ b/Firebase/Database/Core/Operation/FOperation.h
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+@class FOperationSource;
+@class FPath;
+
+typedef NS_ENUM(NSInteger, FOperationType) {
+ FOperationTypeOverwrite = 0,
+ FOperationTypeMerge = 1,
+ FOperationTypeAckUserWrite = 2,
+ FOperationTypeListenComplete = 3
+};
+
+@protocol FOperation <NSObject>
+@property (nonatomic, strong, readonly) FOperationSource *source;
+@property (nonatomic, readonly) FOperationType type;
+@property (nonatomic, strong, readonly) FPath *path;
+- (id<FOperation>) operationForChild:(NSString *)childKey;
+@end
diff --git a/Firebase/Database/Core/Operation/FOperationSource.h b/Firebase/Database/Core/Operation/FOperationSource.h
new file mode 100644
index 0000000..a069c2f
--- /dev/null
+++ b/Firebase/Database/Core/Operation/FOperationSource.h
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+@class FQueryParams;
+
+@interface FOperationSource : NSObject
+
+@property (nonatomic, readonly) BOOL fromUser;
+@property (nonatomic, readonly) BOOL fromServer;
+@property (nonatomic, readonly) BOOL isTagged;
+@property (nonatomic, strong, readonly) FQueryParams *queryParams;
+
+- initWithFromUser:(BOOL)isFromUser fromServer:(BOOL)isFromServer queryParams:(FQueryParams *)params tagged:(BOOL)isTagged;
+
++ (FOperationSource *) userInstance;
++ (FOperationSource *) serverInstance;
++ (FOperationSource *) forServerTaggedQuery:(FQueryParams *)params;
+
+@end
diff --git a/Firebase/Database/Core/Operation/FOperationSource.m b/Firebase/Database/Core/Operation/FOperationSource.m
new file mode 100644
index 0000000..9a34a2e
--- /dev/null
+++ b/Firebase/Database/Core/Operation/FOperationSource.m
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FOperationSource.h"
+#import "FPath.h"
+#import "FQueryParams.h"
+
+@interface FOperationSource ()
+@property (nonatomic, readwrite) BOOL fromUser;
+@property (nonatomic, readwrite) BOOL fromServer;
+@property (nonatomic, readwrite) BOOL isTagged;
+@property (nonatomic, strong, readwrite) FQueryParams *queryParams;
+@end
+
+@implementation FOperationSource
+
+@synthesize fromUser;
+@synthesize fromServer;
+@synthesize queryParams;
+
+- (id) initWithFromUser:(BOOL)isFromUser fromServer:(BOOL)isFromServer queryParams:(FQueryParams *)params tagged:(BOOL)tagged {
+ self = [super init];
+ if (self) {
+ self.fromUser = isFromUser;
+ self.fromServer = isFromServer;
+ self.queryParams = params;
+ self.isTagged = tagged;
+ }
+ return self;
+}
+
++ (FOperationSource *) userInstance {
+ static FOperationSource *user = nil;
+ static dispatch_once_t userToken;
+ dispatch_once(&userToken, ^{
+ user = [[FOperationSource alloc] initWithFromUser:YES fromServer:NO queryParams:nil tagged:NO];
+ });
+ return user;
+}
+
++ (FOperationSource *) serverInstance {
+ static FOperationSource *server = nil;
+ static dispatch_once_t serverToken;
+ dispatch_once(&serverToken, ^{
+ server = [[FOperationSource alloc] initWithFromUser:NO fromServer:YES queryParams:nil tagged:NO];
+ });
+ return server;
+}
+
++ (FOperationSource *) forServerTaggedQuery:(FQueryParams *)params {
+ return [[FOperationSource alloc] initWithFromUser:NO fromServer:YES queryParams:params tagged:YES];
+}
+
+- (NSString *) description {
+ return [NSString stringWithFormat:@"FOperationSource { fromUser=%d, fromServer=%d, queryId=%@, tagged=%d }",
+ self.fromUser, self.fromServer, self.queryParams, self.isTagged];
+}
+
+
+@end
diff --git a/Firebase/Database/Core/Operation/FOverwrite.h b/Firebase/Database/Core/Operation/FOverwrite.h
new file mode 100644
index 0000000..e950bed
--- /dev/null
+++ b/Firebase/Database/Core/Operation/FOverwrite.h
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FOperation.h"
+
+@protocol FNode;
+
+@interface FOverwrite : NSObject <FOperation>
+
+- (id) initWithSource:(FOperationSource *)aSource path:(FPath *)aPath snap:(id<FNode>)aSnap;
+
+@property (nonatomic, strong, readonly) FOperationSource *source;
+@property (nonatomic, readonly) FOperationType type;
+@property (nonatomic, strong, readonly) FPath *path;
+@property (nonatomic, strong, readonly) id<FNode> snap;
+
+@end
diff --git a/Firebase/Database/Core/Operation/FOverwrite.m b/Firebase/Database/Core/Operation/FOverwrite.m
new file mode 100644
index 0000000..b72d31a
--- /dev/null
+++ b/Firebase/Database/Core/Operation/FOverwrite.m
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FOverwrite.h"
+#import "FNode.h"
+#import "FOperationSource.h"
+
+@interface FOverwrite ()
+@property (nonatomic, strong, readwrite) FOperationSource *source;
+@property (nonatomic, readwrite) FOperationType type;
+@property (nonatomic, strong, readwrite) FPath *path;
+@property (nonatomic, strong) id<FNode> snap;
+@end
+
+@implementation FOverwrite
+
+@synthesize source;
+@synthesize type;
+@synthesize path;
+@synthesize snap;
+
+- (id) initWithSource:(FOperationSource *)aSource path:(FPath *)aPath snap:(id <FNode>)aSnap {
+ self = [super init];
+ if (self) {
+ self.source = aSource;
+ self.type = FOperationTypeOverwrite;
+ self.path = aPath;
+ self.snap = aSnap;
+ }
+ return self;
+}
+
+- (FOverwrite *) operationForChild:(NSString *)childKey {
+ if ([self.path isEmpty]) {
+ return [[FOverwrite alloc] initWithSource:self.source
+ path:[FPath empty]
+ snap:[self.snap getImmediateChild:childKey]];
+ } else {
+ return [[FOverwrite alloc] initWithSource:self.source
+ path:[self.path popFront]
+ snap:self.snap];
+ }
+}
+
+- (NSString *) description {
+ return [NSString stringWithFormat:@"FOverwrite { path=%@, source=%@, snapshot=%@ }", self.path, self.source, self.snap];
+}
+
+@end
diff --git a/Firebase/Database/Core/Utilities/FIRRetryHelper.h b/Firebase/Database/Core/Utilities/FIRRetryHelper.h
new file mode 100644
index 0000000..ffe2726
--- /dev/null
+++ b/Firebase/Database/Core/Utilities/FIRRetryHelper.h
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+@interface FIRRetryHelper : NSObject
+
+- (instancetype) initWithDispatchQueue:(dispatch_queue_t)dispatchQueue
+ minRetryDelayAfterFailure:(NSTimeInterval)minRetryDelayAfterFailure
+ maxRetryDelay:(NSTimeInterval)maxRetryDelay
+ retryExponent:(double)retryExponent
+ jitterFactor:(double)jitterFactor;
+
+- (void) retry:(void (^)())block;
+
+- (void) cancel;
+
+- (void) signalSuccess;
+
+@end
diff --git a/Firebase/Database/Core/Utilities/FIRRetryHelper.m b/Firebase/Database/Core/Utilities/FIRRetryHelper.m
new file mode 100644
index 0000000..199e17d
--- /dev/null
+++ b/Firebase/Database/Core/Utilities/FIRRetryHelper.m
@@ -0,0 +1,139 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRRetryHelper.h"
+#import "FUtilities.h"
+
+@interface FIRRetryHelperTask : NSObject
+
+@property (nonatomic, strong) void (^block)();
+
+@end
+
+@implementation FIRRetryHelperTask
+
+- (instancetype) initWithBlock:(void (^)())block {
+ self = [super init];
+ if (self != nil) {
+ self->_block = [block copy];
+ }
+ return self;
+}
+
+- (BOOL) isCanceled {
+ return self.block == nil;
+}
+
+- (void) cancel {
+ self.block = nil;
+}
+
+- (void) execute {
+ if (self.block) {
+ self.block();
+ }
+}
+
+@end
+
+
+
+@interface FIRRetryHelper ()
+
+@property (nonatomic, strong) dispatch_queue_t dispatchQueue;
+@property (nonatomic) NSTimeInterval minRetryDelayAfterFailure;
+@property (nonatomic) NSTimeInterval maxRetryDelay;
+@property (nonatomic) double retryExponent;
+@property (nonatomic) double jitterFactor;
+
+@property (nonatomic) BOOL lastWasSuccess;
+@property (nonatomic) NSTimeInterval currentRetryDelay;
+
+@property (nonatomic, strong) FIRRetryHelperTask *scheduledRetry;
+
+@end
+
+@implementation FIRRetryHelper
+
+- (instancetype) initWithDispatchQueue:(dispatch_queue_t)dispatchQueue
+ minRetryDelayAfterFailure:(NSTimeInterval)minRetryDelayAfterFailure
+ maxRetryDelay:(NSTimeInterval)maxRetryDelay
+ retryExponent:(double)retryExponent
+ jitterFactor:(double)jitterFactor {
+ self = [super init];
+ if (self != nil) {
+ self->_dispatchQueue = dispatchQueue;
+ self->_minRetryDelayAfterFailure = minRetryDelayAfterFailure;
+ self->_maxRetryDelay = maxRetryDelay;
+ self->_retryExponent = retryExponent;
+ self->_jitterFactor = jitterFactor;
+ self->_lastWasSuccess = YES;
+ }
+ return self;
+}
+
+- (void) retry:(void (^)())block {
+ if (self.scheduledRetry != nil) {
+ FFLog(@"I-RDB054001", @"Canceling existing retry attempt");
+ [self.scheduledRetry cancel];
+ self.scheduledRetry = nil;
+ }
+
+ NSTimeInterval delay;
+ if (self.lastWasSuccess) {
+ delay = 0;
+ } else {
+ if (self.currentRetryDelay == 0) {
+ self.currentRetryDelay = self.minRetryDelayAfterFailure;
+ } else {
+ NSTimeInterval newDelay = (self.currentRetryDelay * self.retryExponent);
+ self.currentRetryDelay = MIN(newDelay, self.maxRetryDelay);
+ }
+
+ delay = ((1 - self.jitterFactor) * self.currentRetryDelay) +
+ (self.jitterFactor * self.currentRetryDelay * [FUtilities randomDouble]);
+ FFLog(@"I-RDB054002", @"Scheduling retry in %fs", delay);
+
+ }
+ self.lastWasSuccess = NO;
+ FIRRetryHelperTask *task = [[FIRRetryHelperTask alloc] initWithBlock:block];
+ self.scheduledRetry = task;
+ dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (long long)(delay * NSEC_PER_SEC));
+ dispatch_after(popTime, self.dispatchQueue, ^{
+ if (![task isCanceled]) {
+ self.scheduledRetry = nil;
+ [task execute];
+ }
+ });
+}
+
+- (void) signalSuccess {
+ self.lastWasSuccess = YES;
+ self.currentRetryDelay = 0;
+}
+
+- (void) cancel {
+ if (self.scheduledRetry != nil) {
+ FFLog(@"I-RDB054003", @"Canceling existing retry attempt");
+ [self.scheduledRetry cancel];
+ self.scheduledRetry = nil;
+ } else {
+ FFLog(@"I-RDB054004", @"No existing retry attempt to cancel");
+ }
+ self.currentRetryDelay = 0;
+}
+
+@end
diff --git a/Firebase/Database/Core/Utilities/FImmutableTree.h b/Firebase/Database/Core/Utilities/FImmutableTree.h
new file mode 100644
index 0000000..005a9f2
--- /dev/null
+++ b/Firebase/Database/Core/Utilities/FImmutableTree.h
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FImmutableSortedDictionary.h"
+#import "FPath.h"
+#import "FTuplePathValue.h"
+
+@interface FImmutableTree : NSObject
+
+- (id) initWithValue:(id)aValue;
+- (id) initWithValue:(id)aValue children:(FImmutableSortedDictionary *)childrenMap;
+
++ (FImmutableTree *) empty;
+- (BOOL) isEmpty;
+
+- (FTuplePathValue *) findRootMostMatchingPath:(FPath *)relativePath predicate:(BOOL (^)(id))predicate;
+- (FTuplePathValue *) findRootMostValueAndPath:(FPath *)relativePath;
+- (FImmutableTree *) subtreeAtPath:(FPath *)relativePath;
+- (FImmutableTree *) setValue:(id)newValue atPath:(FPath *)relativePath;
+- (FImmutableTree *) removeValueAtPath:(FPath *)relativePath;
+- (id) valueAtPath:(FPath *)relativePath;
+- (id) rootMostValueOnPath:(FPath *)path;
+- (id) rootMostValueOnPath:(FPath *)path matching:(BOOL (^)(id))predicate;
+- (id) leafMostValueOnPath:(FPath *)path;
+- (id) leafMostValueOnPath:(FPath *)relativePath matching:(BOOL (^)(id))predicate;
+- (BOOL) containsValueMatching:(BOOL (^)(id))predicate;
+- (FImmutableTree *) setTree:(FImmutableTree *)newTree atPath:(FPath *)relativePath;
+- (id) foldWithBlock:(id (^)(FPath *path, id value, NSDictionary *foldedChildren))block;
+- (id) findOnPath:(FPath *)path andApplyBlock:(id (^)(FPath *path, id value))block;
+- (FPath *) forEachOnPath:(FPath *)path whileBlock:(BOOL (^)(FPath *path, id value))block;
+- (FImmutableTree *) forEachOnPath:(FPath *)path performBlock:(void (^)(FPath *path, id value))block;
+- (void) forEach:(void (^)(FPath *path, id value))block;
+- (void) forEachChild:(void (^)(NSString *childKey, id childValue))block;
+
+@property (nonatomic, strong, readonly) id value;
+@property (nonatomic, strong, readonly) FImmutableSortedDictionary *children;
+
+@end
diff --git a/Firebase/Database/Core/Utilities/FImmutableTree.m b/Firebase/Database/Core/Utilities/FImmutableTree.m
new file mode 100644
index 0000000..57bf74d
--- /dev/null
+++ b/Firebase/Database/Core/Utilities/FImmutableTree.m
@@ -0,0 +1,421 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FImmutableTree.h"
+#import "FImmutableSortedDictionary.h"
+#import "FPath.h"
+#import "FUtilities.h"
+
+@interface FImmutableTree ()
+@property (nonatomic, strong, readwrite) id value;
+/**
+* Maps NSString -> FImmutableTree<T>, where <T> is type of value.
+*/
+@property (nonatomic, strong, readwrite) FImmutableSortedDictionary *children;
+@end
+
+@implementation FImmutableTree
+@synthesize value;
+@synthesize children;
+
+- (id) initWithValue:(id)aValue {
+ self = [super init];
+ if (self) {
+ self.value = aValue;
+ self.children = [FImmutableTree emptyChildren];
+ }
+ return self;
+}
+
+- (id) initWithValue:(id)aValue children:(FImmutableSortedDictionary *)childrenMap {
+ self = [super init];
+ if (self) {
+ self.value = aValue;
+ self.children = childrenMap;
+ }
+ return self;
+}
+
++ (FImmutableSortedDictionary *) emptyChildren {
+ static dispatch_once_t emptyChildrenToken;
+ static FImmutableSortedDictionary *emptyChildren;
+ dispatch_once(&emptyChildrenToken, ^{
+ emptyChildren = [FImmutableSortedDictionary dictionaryWithComparator:[FUtilities stringComparator]];
+ });
+ return emptyChildren;
+}
+
++ (FImmutableTree *) empty {
+ static dispatch_once_t emptyImmutableTreeToken;
+ static FImmutableTree *emptyTree = nil;
+ dispatch_once(&emptyImmutableTreeToken, ^{
+ emptyTree = [[FImmutableTree alloc] initWithValue:nil];
+ });
+ return emptyTree;
+}
+
+- (BOOL) isEmpty {
+ return self.value == nil && [self.children isEmpty];
+}
+
+/**
+* Given a path and a predicate, return the first node and the path to that node where the predicate returns true
+* // TODO Do a perf test. If we're creating a bunch of FTuplePathValue objects on the way back out, it may be better to pass down a pathSoFar FPath
+*/
+- (FTuplePathValue *) findRootMostMatchingPath:(FPath *)relativePath predicate:(BOOL (^)(id value))predicate {
+ if (self.value != nil && predicate(self.value)) {
+ return [[FTuplePathValue alloc] initWithPath:[FPath empty] value:self.value];
+ } else {
+ if ([relativePath isEmpty]) {
+ return nil;
+ } else {
+ NSString *front = [relativePath getFront];
+ FImmutableTree *child = [self.children get:front];
+ if (child != nil) {
+ FTuplePathValue *childExistingPathAndValue = [child findRootMostMatchingPath:[relativePath popFront] predicate:predicate];
+ if (childExistingPathAndValue != nil) {
+ FPath *fullPath = [[[FPath alloc] initWith:front] child:childExistingPathAndValue.path];
+ return [[FTuplePathValue alloc] initWithPath:fullPath value:childExistingPathAndValue.value];
+ } else {
+ return nil;
+ }
+ } else {
+ // No child matching path
+ return nil;
+ }
+ }
+ }
+}
+
+/**
+* Find, if it exists, the shortest subpath of the given path that points a defined value in the tree
+*/
+- (FTuplePathValue *) findRootMostValueAndPath:(FPath *)relativePath {
+ return [self findRootMostMatchingPath:relativePath predicate:^BOOL(__unsafe_unretained id value){
+ return YES;
+ }];
+}
+
+- (id) rootMostValueOnPath:(FPath *)path {
+ return [self rootMostValueOnPath:path matching:^BOOL(id value) {
+ return YES;
+ }];
+}
+
+- (id) rootMostValueOnPath:(FPath *)path matching:(BOOL (^)(id))predicate {
+ if (self.value != nil && predicate(self.value)) {
+ return self.value;
+ } else if (path.isEmpty) {
+ return nil;
+ } else {
+ return [[self.children get:path.getFront] rootMostValueOnPath:[path popFront] matching:predicate];
+ }
+}
+
+- (id) leafMostValueOnPath:(FPath *)path {
+ return [self leafMostValueOnPath:path matching:^BOOL(id value) {
+ return YES;
+ }];
+}
+
+- (id) leafMostValueOnPath:(FPath *)relativePath matching:(BOOL (^)(id))predicate {
+ __block id currentValue = self.value;
+ __block FImmutableTree *currentTree = self;
+ [relativePath enumerateComponentsUsingBlock:^(NSString *key, BOOL *stop) {
+ currentTree = [currentTree.children get:key];
+ if (currentTree == nil) {
+ *stop = YES;
+ } else {
+ id treeValue = currentTree.value;
+ if (treeValue != nil && predicate(treeValue)) {
+ currentValue = treeValue;
+ }
+ }
+ }];
+ return currentValue;
+}
+
+- (BOOL) containsValueMatching:(BOOL (^)(id))predicate {
+ if (self.value != nil && predicate(self.value)) {
+ return YES;
+ } else {
+ __block BOOL found = NO;
+ [self.children enumerateKeysAndObjectsUsingBlock:^(NSString *key, FImmutableTree *subtree, BOOL *stop) {
+ found = [subtree containsValueMatching:predicate];
+ if (found) *stop = YES;
+ }];
+ return found;
+ }
+}
+
+- (FImmutableTree *) subtreeAtPath:(FPath *)relativePath {
+ if ([relativePath isEmpty]) {
+ return self;
+ } else {
+ NSString *front = [relativePath getFront];
+ FImmutableTree *childTree = [self.children get:front];
+ if (childTree != nil) {
+ return [childTree subtreeAtPath:[relativePath popFront]];
+ } else {
+ return [FImmutableTree empty];
+ }
+ }
+}
+
+/**
+* Sets a value at the specified path
+*/
+- (FImmutableTree *) setValue:(id)newValue atPath:(FPath *)relativePath {
+ if ([relativePath isEmpty]) {
+ return [[FImmutableTree alloc] initWithValue:newValue children:self.children];
+ } else {
+ NSString *front = [relativePath getFront];
+ FImmutableTree *child = [self.children get:front];
+ if (child == nil) {
+ child = [FImmutableTree empty];
+ }
+ FImmutableTree *newChild = [child setValue:newValue atPath:[relativePath popFront]];
+ FImmutableSortedDictionary *newChildren = [self.children insertKey:front withValue:newChild];
+ return [[FImmutableTree alloc] initWithValue:self.value children:newChildren];
+ }
+}
+
+/**
+* Remove the value at the specified path
+*/
+- (FImmutableTree *) removeValueAtPath:(FPath *)relativePath {
+ if ([relativePath isEmpty]) {
+ if ([self.children isEmpty]) {
+ return [FImmutableTree empty];
+ } else {
+ return [[FImmutableTree alloc] initWithValue:nil children:self.children];
+ }
+ } else {
+ NSString *front = [relativePath getFront];
+ FImmutableTree *child = [self.children get:front];
+ if (child) {
+ FImmutableTree *newChild = [child removeValueAtPath:[relativePath popFront]];
+ FImmutableSortedDictionary *newChildren;
+ if ([newChild isEmpty]) {
+ newChildren = [self.children removeKey:front];
+ } else {
+ newChildren = [self.children insertKey:front withValue:newChild];
+ }
+ if (self.value == nil && [newChildren isEmpty]) {
+ return [FImmutableTree empty];
+ } else {
+ return [[FImmutableTree alloc] initWithValue:self.value children:newChildren];
+ }
+ } else {
+ return self;
+ }
+ }
+}
+
+/**
+* Gets a value from the tree
+*/
+- (id) valueAtPath:(FPath *)relativePath {
+ if ([relativePath isEmpty]) {
+ return self.value;
+ } else {
+ NSString *front = [relativePath getFront];
+ FImmutableTree *child = [self.children get:front];
+ if (child) {
+ return [child valueAtPath:[relativePath popFront]];
+ } else {
+ return nil;
+ }
+ }
+}
+
+/**
+* Replaces the subtree at the specified path with the given new tree
+*/
+- (FImmutableTree *) setTree:(FImmutableTree *)newTree atPath:(FPath *)relativePath {
+ if ([relativePath isEmpty]) {
+ return newTree;
+ } else {
+ NSString *front = [relativePath getFront];
+ FImmutableTree *child = [self.children get:front];
+ if (child == nil) {
+ child = [FImmutableTree empty];
+ }
+ FImmutableTree *newChild = [child setTree:newTree atPath:[relativePath popFront]];
+ FImmutableSortedDictionary *newChildren;
+ if ([newChild isEmpty]) {
+ newChildren = [self.children removeKey:front];
+ } else {
+ newChildren = [self.children insertKey:front withValue:newChild];
+ }
+ return [[FImmutableTree alloc] initWithValue:self.value children:newChildren];
+ }
+}
+
+/**
+* Performs a depth first fold on this tree. Transforms a tree into a single value, given a function that operates on
+* the path to a node, an optional current value, and a map of the child names to folded subtrees
+*/
+- (id) foldWithBlock:(id (^)(FPath *path, id value, NSDictionary *foldedChildren))block {
+ return [self foldWithPathSoFar:[FPath empty] withBlock:block];
+}
+
+/**
+* Recursive helper for public facing foldWithBlock: method
+*/
+- (id) foldWithPathSoFar:(FPath *)pathSoFar withBlock:(id (^)(FPath *path, id value, NSDictionary *foldedChildren))block {
+ __block NSMutableDictionary *accum = [[NSMutableDictionary alloc] init];
+ [self.children enumerateKeysAndObjectsUsingBlock:^(NSString *childKey, FImmutableTree *childTree, BOOL *stop) {
+ accum[childKey] = [childTree foldWithPathSoFar:[pathSoFar childFromString:childKey] withBlock:block];
+ }];
+ return block(pathSoFar, self.value, accum);
+}
+
+/**
+* Find the first matching value on the given path. Return the result of applying block to it.
+*/
+- (id) findOnPath:(FPath *)path andApplyBlock:(id (^)(FPath *path, id value))block {
+ return [self findOnPath:path pathSoFar:[FPath empty] andApplyBlock:block];
+}
+
+- (id) findOnPath:(FPath *)pathToFollow pathSoFar:(FPath *)pathSoFar andApplyBlock:(id (^)(FPath *path, id value))block {
+ id result = self.value ? block(pathSoFar, self.value) : nil;
+ if (result != nil) {
+ return result;
+ } else {
+ if ([pathToFollow isEmpty]) {
+ return nil;
+ } else {
+ NSString *front = [pathToFollow getFront];
+ FImmutableTree *nextChild = [self.children get:front];
+ if (nextChild != nil) {
+ return [nextChild findOnPath:[pathToFollow popFront] pathSoFar:[pathSoFar childFromString:front] andApplyBlock:block];
+ } else {
+ return nil;
+ }
+ }
+ }
+}
+/**
+* Call the block on each value along the path for as long as that function returns true
+* @return The path to the deepest location inspected
+*/
+- (FPath *) forEachOnPath:(FPath *)path whileBlock:(BOOL (^)(FPath *, id))block {
+ return [self forEachOnPath:path pathSoFar:[FPath empty] whileBlock:block];
+}
+
+- (FPath *) forEachOnPath:(FPath *)pathToFollow pathSoFar:(FPath *)pathSoFar whileBlock:(BOOL (^)(FPath *, id))block {
+ if ([pathToFollow isEmpty]) {
+ if (self.value) {
+ block(pathSoFar, self.value);
+ }
+ return pathSoFar;
+ } else {
+ BOOL shouldContinue = YES;
+ if (self.value) {
+ shouldContinue = block(pathSoFar, self.value);
+ }
+ if (shouldContinue) {
+ NSString *front = [pathToFollow getFront];
+ FImmutableTree *nextChild = [self.children get:front];
+ if (nextChild) {
+ return [nextChild forEachOnPath:[pathToFollow popFront] pathSoFar:[pathSoFar childFromString:front] whileBlock:block];
+ } else {
+ return pathSoFar;
+ }
+ } else {
+ return pathSoFar;
+ }
+ }
+}
+
+- (FImmutableTree *) forEachOnPath:(FPath *)path performBlock:(void (^)(FPath *path, id value))block {
+ return [self forEachOnPath:path pathSoFar:[FPath empty] performBlock:block];
+}
+
+- (FImmutableTree *) forEachOnPath:(FPath *)pathToFollow pathSoFar:(FPath *)pathSoFar performBlock:(void (^)(FPath *path, id value))block {
+ if ([pathToFollow isEmpty]) {
+ return self;
+ } else {
+ if (self.value) {
+ block(pathSoFar, self.value);
+ }
+ NSString *front = [pathToFollow getFront];
+ FImmutableTree *nextChild = [self.children get:front];
+ if (nextChild) {
+ return [nextChild forEachOnPath:[pathToFollow popFront] pathSoFar:[pathSoFar childFromString:front] performBlock:block];
+ } else {
+ return [FImmutableTree empty];
+ }
+ }
+}
+/**
+* Calls the given block for each node in the tree that has a value. Called in depth-first order
+*/
+- (void) forEach:(void (^)(FPath *path, id value))block {
+ [self forEachPathSoFar:[FPath empty] withBlock:block];
+}
+
+- (void) forEachPathSoFar:(FPath *)pathSoFar withBlock:(void (^)(FPath *path, id value))block {
+ [self.children enumerateKeysAndObjectsUsingBlock:^(NSString *childKey, FImmutableTree *childTree, BOOL *stop) {
+ [childTree forEachPathSoFar:[pathSoFar childFromString:childKey] withBlock:block];
+ }];
+ if (self.value) {
+ block(pathSoFar, self.value);
+ }
+}
+
+- (void) forEachChild:(void (^)(NSString *childKey, id childValue))block {
+ [self.children enumerateKeysAndObjectsUsingBlock:^(NSString *childKey, FImmutableTree *childTree, BOOL *stop) {
+ if (childTree.value) {
+ block(childKey, childTree.value);
+ }
+ }];
+}
+
+- (BOOL)isEqual:(id)object {
+ if (![object isKindOfClass:[FImmutableTree class]]) {
+ return NO;
+ }
+ FImmutableTree *other = (FImmutableTree *)object;
+ return (self.value == other.value || [self.value isEqual:other.value]) && [self.children isEqual:other.children];
+}
+
+- (NSUInteger)hash {
+ return self.children.hash * 31 + [self.value hash];
+}
+
+- (NSString *) description {
+ NSMutableString *string = [[NSMutableString alloc] init];
+ [string appendString:@"FImmutableTree { value="];
+ [string appendString:(self.value ? [self.value description] : @"<nil>")];
+ [string appendString:@", children={"];
+ [self.children enumerateKeysAndObjectsUsingBlock:^(NSString *childKey, FImmutableTree *childTree, BOOL *stop) {
+ [string appendString:@" "];
+ [string appendString:childKey];
+ [string appendString:@"="];
+ [string appendString:[childTree.value description]];
+ }];
+ [string appendString:@" } }"];
+ return [NSString stringWithString:string];
+}
+
+- (NSString *) debugDescription {
+ return [self description];
+}
+
+
+@end
diff --git a/Firebase/Database/Core/Utilities/FPath.h b/Firebase/Database/Core/Utilities/FPath.h
new file mode 100644
index 0000000..71a7167
--- /dev/null
+++ b/Firebase/Database/Core/Utilities/FPath.h
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+@interface FPath : NSObject<NSCopying>
+
++ (FPath *) relativePathFrom:(FPath *)outer to:(FPath *)inner;
++ (FPath *) empty;
++ (FPath *) pathWithString:(NSString *)string;
+
+- (id)initWith:(NSString *)path;
+- (id)initWithPieces:(NSArray *)somePieces andPieceNum:(NSInteger)aPieceNum;
+
+- (id)copyWithZone:(NSZone *)zone;
+
+- (void)enumerateComponentsUsingBlock:(void (^)(NSString *key, BOOL *stop))block;
+- (NSString *) getFront;
+- (NSUInteger) length;
+- (FPath *) popFront;
+- (NSString *) getBack;
+- (NSString *) toString;
+- (NSString *) toStringWithTrailingSlash;
+- (NSString *) wireFormat;
+- (FPath *) parent;
+- (FPath *) child:(FPath *)childPathObj;
+- (FPath *) childFromString:(NSString *)childPath;
+- (BOOL) isEmpty;
+- (BOOL) contains:(FPath *)other;
+- (NSComparisonResult) compare:(FPath *)other;
+
+@end
diff --git a/Firebase/Database/Core/Utilities/FPath.m b/Firebase/Database/Core/Utilities/FPath.m
new file mode 100644
index 0000000..485b903
--- /dev/null
+++ b/Firebase/Database/Core/Utilities/FPath.m
@@ -0,0 +1,298 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FPath.h"
+
+#import "FUtilities.h"
+
+@interface FPath()
+
+@property (nonatomic, readwrite, assign) NSInteger pieceNum;
+@property (nonatomic, strong) NSArray * pieces;
+
+@end
+
+@implementation FPath
+
+#pragma mark -
+#pragma mark Initializers
+
++ (FPath *) relativePathFrom:(FPath *)outer to:(FPath *)inner {
+ NSString* outerFront = [outer getFront];
+ NSString* innerFront = [inner getFront];
+ if (outerFront == nil) {
+ return inner;
+ } else if ([outerFront isEqualToString:innerFront]) {
+ return [self relativePathFrom:[outer popFront] to:[inner popFront]];
+ } else {
+ @throw [[NSException alloc] initWithName:@"FirebaseDatabaseInternalError" reason:[NSString stringWithFormat:@"innerPath (%@) is not within outerPath (%@)", inner, outer] userInfo:nil];
+ }
+}
+
++ (FPath *)pathWithString:(NSString *)string
+{
+ return [[FPath alloc] initWith:string];
+}
+
+- (id)initWith:(NSString *)path
+{
+ self = [super init];
+ if (self) {
+ NSArray *pathPieces = [path componentsSeparatedByString:@"/"];
+ NSMutableArray *newPieces = [[NSMutableArray alloc] init];
+ for (NSInteger i = 0; i < pathPieces.count; i++) {
+ NSString *piece = [pathPieces objectAtIndex:i];
+ if (piece.length > 0) {
+ [newPieces addObject:piece];
+ }
+ }
+
+ self.pieces = newPieces;
+ self.pieceNum = 0;
+ }
+ return self;
+}
+
+- (id)initWithPieces:(NSArray *)somePieces andPieceNum:(NSInteger)aPieceNum {
+ self = [super init];
+ if (self) {
+ self.pieceNum = aPieceNum;
+ self.pieces = somePieces;
+ }
+ return self;
+}
+
+- (id)copyWithZone:(NSZone *)zone
+{
+ // Immutable, so it's safe to return self
+ return self;
+}
+
+- (NSString *)description {
+ return [self toString];
+}
+
+#pragma mark -
+#pragma mark Public methods
+
+- (NSString *) getFront {
+ if(self.pieceNum >= self.pieces.count) {
+ return nil;
+ }
+ return [self.pieces objectAtIndex:self.pieceNum];
+}
+
+/**
+* @return The number of segments in this path
+*/
+- (NSUInteger) length {
+ return self.pieces.count - self.pieceNum;
+}
+
+- (FPath *) popFront {
+ NSInteger newPieceNum = self.pieceNum;
+ if (newPieceNum < self.pieces.count) {
+ newPieceNum++;
+ }
+ return [[FPath alloc] initWithPieces:self.pieces andPieceNum:newPieceNum];
+}
+
+- (NSString *) getBack {
+ if(self.pieceNum < self.pieces.count) {
+ return [self.pieces lastObject];
+ }
+ else {
+ return nil;
+ }
+}
+
+- (NSString *) toString {
+ return [self toStringWithTrailingSlash:NO];
+}
+
+- (NSString *) toStringWithTrailingSlash {
+ return [self toStringWithTrailingSlash:YES];
+}
+
+- (NSString *) toStringWithTrailingSlash:(BOOL)trailingSlash {
+ NSMutableString* pathString = [[NSMutableString alloc] init];
+ for(NSInteger i = self.pieceNum; i < self.pieces.count; i++) {
+ [pathString appendString:@"/"];
+ [pathString appendString:[self.pieces objectAtIndex:i]];
+ }
+ if ([pathString length] == 0) {
+ return @"/";
+ } else {
+ if (trailingSlash) {
+ [pathString appendString:@"/"];
+ }
+ return pathString;
+ }
+}
+
+- (NSString *)wireFormat {
+ if ([self isEmpty]) {
+ return @"/";
+ } else {
+ NSMutableString* pathString = [[NSMutableString alloc] init];
+ for (NSInteger i = self.pieceNum; i < self.pieces.count; i++) {
+ if (i > self.pieceNum) {
+ [pathString appendString:@"/"];
+ }
+ [pathString appendString:[self.pieces objectAtIndex:i]];
+ }
+ return pathString;
+ }
+}
+
+- (FPath *) parent {
+ if(self.pieceNum >= self.pieces.count) {
+ return nil;
+ } else {
+ NSMutableArray* newPieces = [[NSMutableArray alloc] init];
+ for (NSInteger i = self.pieceNum; i < self.pieces.count - 1; i++) {
+ [newPieces addObject:[self.pieces objectAtIndex:i]];
+ }
+ return [[FPath alloc] initWithPieces:newPieces andPieceNum:0];
+ }
+}
+
+- (FPath *) child:(FPath *)childPathObj {
+ NSMutableArray* newPieces = [[NSMutableArray alloc] init];
+ for (NSInteger i = self.pieceNum; i < self.pieces.count; i++) {
+ [newPieces addObject:[self.pieces objectAtIndex:i]];
+ }
+
+ for (NSInteger i = childPathObj.pieceNum; i < childPathObj.pieces.count; i++) {
+ [newPieces addObject:[childPathObj.pieces objectAtIndex:i]];
+ }
+
+ return [[FPath alloc] initWithPieces:newPieces andPieceNum:0];
+}
+
+- (FPath *)childFromString:(NSString *)childPath {
+ NSMutableArray* newPieces = [[NSMutableArray alloc] init];
+ for (NSInteger i = self.pieceNum; i < self.pieces.count; i++) {
+ [newPieces addObject:[self.pieces objectAtIndex:i]];
+ }
+
+ NSArray *pathPieces = [childPath componentsSeparatedByString:@"/"];
+ for (unsigned int i = 0; i < pathPieces.count; i++) {
+ NSString *piece = [pathPieces objectAtIndex:i];
+ if (piece.length > 0) {
+ [newPieces addObject:piece];
+ }
+ }
+
+ return [[FPath alloc] initWithPieces:newPieces andPieceNum:0];
+}
+
+/**
+* @return True if there are no segments in this path
+*/
+- (BOOL) isEmpty {
+ return self.pieceNum >= self.pieces.count;
+}
+
+/**
+* @return Singleton to represent an empty path
+*/
++ (FPath *) empty {
+ static dispatch_once_t oneEmptyPath;
+ static FPath *emptyPath;
+ dispatch_once(&oneEmptyPath, ^{
+ emptyPath = [[FPath alloc] initWith:@""];
+ });
+ return emptyPath;
+}
+
+- (BOOL) contains:(FPath *)other {
+ if (self.length > other.length) {
+ return NO;
+ }
+
+ NSInteger i = self.pieceNum;
+ NSInteger j = other.pieceNum;
+ while (i < self.pieces.count) {
+ NSString* thisSeg = [self.pieces objectAtIndex:i];
+ NSString* otherSeg = [other.pieces objectAtIndex:j];
+ if (![thisSeg isEqualToString:otherSeg]) {
+ return NO;
+ }
+ ++i;
+ ++j;
+ }
+ return YES;
+}
+
+- (void) enumerateComponentsUsingBlock:(void (^)(NSString *, BOOL *))block {
+ BOOL stop = NO;
+ for (NSInteger i = self.pieceNum; !stop && i < self.pieces.count; i++) {
+ block(self.pieces[i], &stop);
+ }
+}
+
+- (NSComparisonResult) compare:(FPath *)other {
+ NSInteger myCount = self.pieces.count;
+ NSInteger otherCount = other.pieces.count;
+ for (NSInteger i = self.pieceNum, j = other.pieceNum; i < myCount && j < otherCount; i++, j++) {
+ NSComparisonResult comparison = [FUtilities compareKey:self.pieces[i] toKey:other.pieces[j]];
+ if (comparison != NSOrderedSame) {
+ return comparison;
+ }
+ }
+ if (self.length < other.length) {
+ return NSOrderedAscending;
+ } else if (other.length < self.length) {
+ return NSOrderedDescending;
+ } else {
+ NSAssert(self.length == other.length, @"Paths must be the same lengths");
+ return NSOrderedSame;
+ }
+}
+
+/**
+* @return YES if paths are the same
+*/
+- (BOOL)isEqual:(id)other
+{
+ if (other == self) {
+ return YES;
+ }
+ if (!other || ![other isKindOfClass:[self class]]) {
+ return NO;
+ }
+ FPath *otherPath = (FPath *)other;
+ if (self.length != otherPath.length) {
+ return NO;
+ }
+ for (NSUInteger i = self.pieceNum, j = otherPath.pieceNum; i < self.pieces.count; i++, j++) {
+ if (![self.pieces[i] isEqualToString:otherPath.pieces[j]]) {
+ return NO;
+ }
+ }
+ return YES;
+}
+
+- (NSUInteger) hash {
+ NSUInteger hashCode = 0;
+ for (NSInteger i = self.pieceNum; i < self.pieces.count; i++) {
+ hashCode = hashCode * 37 + [self.pieces[i] hash];
+ }
+ return hashCode;
+}
+
+@end
diff --git a/Firebase/Database/Core/Utilities/FTree.h b/Firebase/Database/Core/Utilities/FTree.h
new file mode 100644
index 0000000..8528526
--- /dev/null
+++ b/Firebase/Database/Core/Utilities/FTree.h
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+#import "FTreeNode.h"
+#import "FPath.h"
+
+@interface FTree : NSObject
+
+- (id)init;
+- (id)initWithName:(NSString*)aName withParent:(FTree *)aParent withNode:(FTreeNode *)aNode;
+
+- (FTree *) subTree:(FPath*)path;
+- (id)getValue;
+- (void)setValue:(id)value;
+- (void) clear;
+- (BOOL) hasChildren;
+- (BOOL) isEmpty;
+- (void) forEachChildMutationSafe:(void (^)(FTree *))action;
+- (void) forEachChild:(void (^)(FTree *))action;
+- (void) forEachDescendant:(void (^)(FTree *))action;
+- (void) forEachDescendant:(void (^)(FTree *))action includeSelf:(BOOL)incSelf childrenFirst:(BOOL)childFirst;
+- (BOOL) forEachAncestor:(BOOL (^)(FTree *))action;
+- (BOOL) forEachAncestor:(BOOL (^)(FTree *))action includeSelf:(BOOL)incSelf;
+- (void) forEachImmediateDescendantWithValue:(void (^)(FTree *))action;
+- (BOOL) valueExistsAtOrAbove:(FPath *)path;
+- (FPath *)path;
+- (void) updateParents;
+- (void) updateChild:(NSString*)childName withNode:(FTree *)child;
+
+@property (nonatomic, strong) NSString* name;
+@property (nonatomic, strong) FTree* parent;
+@property (nonatomic, strong) FTreeNode* node;
+
+@end
diff --git a/Firebase/Database/Core/Utilities/FTree.m b/Firebase/Database/Core/Utilities/FTree.m
new file mode 100644
index 0000000..8576ffb
--- /dev/null
+++ b/Firebase/Database/Core/Utilities/FTree.m
@@ -0,0 +1,183 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FTree.h"
+#import "FTreeNode.h"
+#import "FPath.h"
+#import "FUtilities.h"
+
+@implementation FTree
+
+@synthesize name;
+@synthesize parent;
+@synthesize node;
+
+- (id)init
+{
+ self = [super init];
+ if (self) {
+ self.name = @"";
+ self.parent = nil;
+ self.node = [[FTreeNode alloc] init];
+ }
+ return self;
+}
+
+
+- (id)initWithName:(NSString*)aName withParent:(FTree *)aParent withNode:(FTreeNode *)aNode
+{
+ self = [super init];
+ if (self) {
+ self.name = aName != nil ? aName : @"";
+ self.parent = aParent != nil ? aParent : nil;
+ self.node = aNode != nil ? aNode : [[FTreeNode alloc] init];
+ }
+ return self;
+}
+
+- (FTree *) subTree:(FPath*)path {
+ FTree* child = self;
+ NSString* next = [path getFront];
+ while(next != nil) {
+ FTreeNode* childNode = child.node.children[next];
+ if (childNode == nil) {
+ childNode = [[FTreeNode alloc] init];
+ }
+ child = [[FTree alloc] initWithName:next withParent:child withNode:childNode];
+ path = [path popFront];
+ next = [path getFront];
+ }
+ return child;
+}
+
+- (id)getValue {
+ return self.node.value;
+}
+
+- (void)setValue:(id)value {
+ self.node.value = value;
+ [self updateParents];
+}
+
+- (void) clear {
+ self.node.value = nil;
+ [self.node.children removeAllObjects];
+ self.node.childCount = 0;
+ [self updateParents];
+}
+
+- (BOOL) hasChildren {
+ return self.node.childCount > 0;
+}
+
+- (BOOL) isEmpty {
+ return [self getValue] == nil && ![self hasChildren];
+}
+
+- (void) forEachChild:(void (^)(FTree *))action {
+ for(NSString* key in self.node.children) {
+ action([[FTree alloc] initWithName:key withParent:self withNode:[self.node.children objectForKey:key]]);
+ }
+}
+
+- (void) forEachChildMutationSafe:(void (^)(FTree *))action {
+ for(NSString* key in [self.node.children copy]) {
+ action([[FTree alloc] initWithName:key withParent:self withNode:[self.node.children objectForKey:key]]);
+ }
+}
+
+- (void) forEachDescendant:(void (^)(FTree *))action {
+ [self forEachDescendant:action includeSelf:NO childrenFirst:NO];
+}
+
+- (void) forEachDescendant:(void (^)(FTree *))action includeSelf:(BOOL)incSelf childrenFirst:(BOOL)childFirst {
+ if(incSelf && !childFirst) {
+ action(self);
+ }
+
+ [self forEachChild:^(FTree* child) {
+ [child forEachDescendant:action includeSelf:YES childrenFirst:childFirst];
+ }];
+
+ if(incSelf && childFirst) {
+ action(self);
+ }
+}
+
+- (BOOL) forEachAncestor:(BOOL (^)(FTree *))action {
+ return [self forEachAncestor:action includeSelf:NO];
+}
+
+- (BOOL) forEachAncestor:(BOOL (^)(FTree *))action includeSelf:(BOOL)incSelf {
+ FTree* aNode = (incSelf) ? self : self.parent;
+ while(aNode != nil) {
+ if(action(aNode)) {
+ return YES;
+ }
+ aNode = aNode.parent;
+ }
+ return NO;
+}
+
+- (void) forEachImmediateDescendantWithValue:(void (^)(FTree *))action {
+ [self forEachChild:^(FTree * child) {
+ if([child getValue] != nil) {
+ action(child);
+ }
+ else {
+ [child forEachImmediateDescendantWithValue:action];
+ }
+ }];
+}
+
+- (BOOL) valueExistsAtOrAbove:(FPath *)path {
+ FTreeNode* aNode = self.node;
+ while(aNode != nil) {
+ if(aNode.value != nil) {
+ return YES;
+ }
+ aNode = [aNode.children objectForKey:path.getFront];
+ path = [path popFront];
+ }
+ // XXX Check with Michael if this is correct; deviates from JS.
+ return NO;
+}
+
+- (FPath *)path {
+ return [[FPath alloc] initWith:(self.parent == nil) ? self.name :
+ [NSString stringWithFormat:@"%@/%@", [self.parent path], self.name ]];
+}
+
+- (void) updateParents {
+ [self.parent updateChild:self.name withNode:self];
+}
+
+- (void) updateChild:(NSString*)childName withNode:(FTree *)child {
+ BOOL childEmpty = [child isEmpty];
+ BOOL childExists = self.node.children[childName] != nil;
+ if(childEmpty && childExists) {
+ [self.node.children removeObjectForKey:childName];
+ self.node.childCount = self.node.childCount - 1;
+ [self updateParents];
+ }
+ else if(!childEmpty && !childExists) {
+ [self.node.children setObject:child.node forKey:childName];
+ self.node.childCount = self.node.childCount + 1;
+ [self updateParents];
+ }
+}
+
+@end
diff --git a/Firebase/Database/Core/Utilities/FTreeNode.h b/Firebase/Database/Core/Utilities/FTreeNode.h
new file mode 100644
index 0000000..7e3497e
--- /dev/null
+++ b/Firebase/Database/Core/Utilities/FTreeNode.h
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+@interface FTreeNode : NSObject
+
+@property (nonatomic, strong) NSMutableDictionary* children;
+@property (nonatomic, readwrite, assign) int childCount;
+@property (nonatomic, strong) id value;
+
+@end
diff --git a/Firebase/Database/Core/Utilities/FTreeNode.m b/Firebase/Database/Core/Utilities/FTreeNode.m
new file mode 100644
index 0000000..9cba9c5
--- /dev/null
+++ b/Firebase/Database/Core/Utilities/FTreeNode.m
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FTreeNode.h"
+
+@implementation FTreeNode
+
+@synthesize children;
+@synthesize childCount;
+@synthesize value;
+
+- (id)init
+{
+ self = [super init];
+ if (self) {
+ self.children = [[NSMutableDictionary alloc] init];
+ self.childCount = 0;
+ self.value = nil;
+ }
+ return self;
+}
+
+@end
diff --git a/Firebase/Database/Core/View/FCacheNode.h b/Firebase/Database/Core/View/FCacheNode.h
new file mode 100644
index 0000000..b23869c
--- /dev/null
+++ b/Firebase/Database/Core/View/FCacheNode.h
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+@protocol FNode;
+@class FIndexedNode;
+@class FPath;
+
+/**
+* A cache node only stores complete children. Additionally it holds a flag whether the node can be considered fully
+* initialized in the sense that we know at one point in time, this represented a valid state of the world, e.g.
+* initialized with data from the server, or a complete overwrite by the client. It is not necessarily complete because
+* it may have been from a tagged query. The filtered flag also tracks whether a node potentially had children removed
+* due to a filter.
+*/
+@interface FCacheNode : NSObject
+
+- (id) initWithIndexedNode:(FIndexedNode *)indexedNode
+ isFullyInitialized:(BOOL)fullyInitialized
+ isFiltered:(BOOL)filtered;
+
+- (BOOL) isCompleteForPath:(FPath *)path;
+- (BOOL) isCompleteForChild:(NSString *)childKey;
+
+@property (nonatomic, readonly) BOOL isFullyInitialized;
+@property (nonatomic, readonly) BOOL isFiltered;
+@property (nonatomic, strong, readonly) FIndexedNode *indexedNode;
+@property (nonatomic, strong, readonly) id<FNode> node;
+
+@end
diff --git a/Firebase/Database/Core/View/FCacheNode.m b/Firebase/Database/Core/View/FCacheNode.m
new file mode 100644
index 0000000..4767a25
--- /dev/null
+++ b/Firebase/Database/Core/View/FCacheNode.m
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FCacheNode.h"
+#import "FNode.h"
+#import "FPath.h"
+#import "FEmptyNode.h"
+#import "FIndexedNode.h"
+
+@interface FCacheNode ()
+@property (nonatomic, readwrite) BOOL isFullyInitialized;
+@property (nonatomic, readwrite) BOOL isFiltered;
+@property (nonatomic, strong, readwrite) FIndexedNode *indexedNode;
+@end
+
+@implementation FCacheNode
+- (id) initWithIndexedNode:(FIndexedNode *)indexedNode
+ isFullyInitialized:(BOOL)fullyInitialized
+ isFiltered:(BOOL)filtered
+{
+ self = [super init];
+ if (self) {
+ self.indexedNode = indexedNode;
+ self.isFullyInitialized = fullyInitialized;
+ self.isFiltered = filtered;
+ }
+ return self;
+}
+
+- (BOOL)isCompleteForPath:(FPath *)path {
+ if (path.isEmpty) {
+ return self.isFullyInitialized && !self.isFiltered;
+ } else {
+ NSString *childKey = [path getFront];
+ return [self isCompleteForChild:childKey];
+ }
+}
+
+- (BOOL)isCompleteForChild:(NSString *)childKey {
+ return (self.isFullyInitialized && !self.isFiltered) || [self.node hasChild:childKey];
+}
+
+- (id<FNode>)node {
+ return self.indexedNode.node;
+}
+
+@end
diff --git a/Firebase/Database/Core/View/FCancelEvent.h b/Firebase/Database/Core/View/FCancelEvent.h
new file mode 100644
index 0000000..38277f7
--- /dev/null
+++ b/Firebase/Database/Core/View/FCancelEvent.h
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+#import "FEvent.h"
+
+@protocol FEventRegistration;
+
+
+@interface FCancelEvent : NSObject<FEvent>
+
+- initWithEventRegistration:(id<FEventRegistration>)eventRegistration error:(NSError *)error path:(FPath *)path;
+
+@property (nonatomic, strong, readonly) NSError *error;
+@property (nonatomic, strong, readonly) FPath *path;
+
+@end
diff --git a/Firebase/Database/Core/View/FCancelEvent.m b/Firebase/Database/Core/View/FCancelEvent.m
new file mode 100644
index 0000000..fb73f17
--- /dev/null
+++ b/Firebase/Database/Core/View/FCancelEvent.m
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FCancelEvent.h"
+#import "FPath.h"
+#import "FEventRegistration.h"
+
+@interface FCancelEvent ()
+@property (nonatomic, strong) id<FEventRegistration> eventRegistration;
+@property (nonatomic, strong, readwrite) NSError *error;
+@property (nonatomic, strong, readwrite) FPath *path;
+@end
+
+@implementation FCancelEvent
+
+@synthesize eventRegistration;
+@synthesize error;
+@synthesize path;
+
+- (id)initWithEventRegistration:(id <FEventRegistration>)registration error:(NSError *)anError path:(FPath *)aPath {
+ self = [super init];
+ if (self) {
+ self.eventRegistration = registration;
+ self.error = anError;
+ self.path = aPath;
+ }
+ return self;
+}
+
+- (void) fireEventOnQueue:(dispatch_queue_t)queue {
+ [self.eventRegistration fireEvent:self queue:queue];
+}
+
+- (BOOL) isCancelEvent {
+ return YES;
+}
+
+- (NSString *) description {
+ return [NSString stringWithFormat:@"%@: cancel", self.path];
+}
+
+@end
diff --git a/Firebase/Database/Core/View/FChange.h b/Firebase/Database/Core/View/FChange.h
new file mode 100644
index 0000000..d728fe0
--- /dev/null
+++ b/Firebase/Database/Core/View/FChange.h
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+#import "FIRDatabaseReference.h"
+#import "FNode.h"
+#import "FIndexedNode.h"
+
+@interface FChange : NSObject
+
+@property (nonatomic, readonly) FIRDataEventType type;
+@property (nonatomic, strong, readonly) FIndexedNode *indexedNode;
+@property (nonatomic, strong, readonly) NSString *childKey;
+@property (nonatomic, strong, readonly) NSString *prevKey;
+@property (nonatomic, strong, readonly) FIndexedNode *oldIndexedNode;
+
+- (id)initWithType:(FIRDataEventType)type indexedNode:(FIndexedNode *)indexedNode;
+- (id)initWithType:(FIRDataEventType)type indexedNode:(FIndexedNode *)indexedNode childKey:(NSString *)childKey;
+- (id)initWithType:(FIRDataEventType)type
+ indexedNode:(FIndexedNode *)indexedNode
+ childKey:(NSString *)childKey
+ oldIndexedNode:(FIndexedNode *)oldIndexedNode;
+
+- (FChange *) changeWithPrevKey:(NSString *)prevKey;
+@end
diff --git a/Firebase/Database/Core/View/FChange.m b/Firebase/Database/Core/View/FChange.m
new file mode 100644
index 0000000..893fce4
--- /dev/null
+++ b/Firebase/Database/Core/View/FChange.m
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FChange.h"
+
+@interface FChange ()
+
+@property (nonatomic, strong, readwrite) NSString *prevKey;
+
+@end
+
+@implementation FChange
+
+- (id)initWithType:(FIRDataEventType)type indexedNode:(FIndexedNode *)indexedNode
+{
+ return [self initWithType:type indexedNode:indexedNode childKey:nil oldIndexedNode:nil];
+}
+
+- (id)initWithType:(FIRDataEventType)type indexedNode:(FIndexedNode *)indexedNode childKey:(NSString *)childKey
+{
+ return [self initWithType:type indexedNode:indexedNode childKey:childKey oldIndexedNode:nil];
+}
+
+- (id)initWithType:(FIRDataEventType)type
+ indexedNode:(FIndexedNode *)indexedNode
+ childKey:(NSString *)childKey
+ oldIndexedNode:(FIndexedNode *)oldIndexedNode
+{
+ self = [super init];
+ if (self != nil) {
+ self->_type = type;
+ self->_indexedNode = indexedNode;
+ self->_childKey = childKey;
+ self->_oldIndexedNode = oldIndexedNode;
+ }
+ return self;
+}
+
+- (FChange *) changeWithPrevKey:(NSString *)prevKey {
+ FChange *newChange = [[FChange alloc] initWithType:self.type
+ indexedNode:self.indexedNode
+ childKey:self.childKey
+ oldIndexedNode:self.oldIndexedNode];
+ newChange.prevKey = prevKey;
+ return newChange;
+}
+
+- (NSString *) description {
+ return [NSString stringWithFormat:@"event: %d, data: %@", (int)self.type, [self.indexedNode.node val]];
+}
+
+@end
diff --git a/Firebase/Database/Core/View/FChildEventRegistration.h b/Firebase/Database/Core/View/FChildEventRegistration.h
new file mode 100644
index 0000000..8da0b8f
--- /dev/null
+++ b/Firebase/Database/Core/View/FChildEventRegistration.h
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+#import "FEventRegistration.h"
+#import "FTypedefs.h"
+
+@class FRepo;
+
+@interface FChildEventRegistration : NSObject <FEventRegistration>
+
+- (id) initWithRepo:(FRepo *)repo
+ handle:(FIRDatabaseHandle)fHandle
+ callbacks:(NSDictionary *)callbackBlocks
+ cancelCallback:(fbt_void_nserror)cancelCallbackBlock;
+
+/**
+* Maps FIRDataEventType (as NSNumber) to fbt_void_datasnapshot_nsstring
+*/
+@property (nonatomic, copy, readonly) NSDictionary *callbacks;
+@property (nonatomic, copy, readonly) fbt_void_nserror cancelCallback;
+@property (nonatomic, readonly) FIRDatabaseHandle handle;
+
+@end
diff --git a/Firebase/Database/Core/View/FChildEventRegistration.m b/Firebase/Database/Core/View/FChildEventRegistration.m
new file mode 100644
index 0000000..6308a90
--- /dev/null
+++ b/Firebase/Database/Core/View/FChildEventRegistration.m
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FChildEventRegistration.h"
+#import "FIRDatabaseQuery_Private.h"
+#import "FQueryParams.h"
+#import "FQuerySpec.h"
+#import "FIRDataSnapshot_Private.h"
+#import "FDataEvent.h"
+#import "FCancelEvent.h"
+
+@interface FChildEventRegistration ()
+@property (nonatomic, strong) FRepo *repo;
+@property (nonatomic, copy, readwrite) NSDictionary *callbacks;
+@property (nonatomic, copy, readwrite) fbt_void_nserror cancelCallback;
+@property (nonatomic, readwrite) FIRDatabaseHandle handle;
+@end
+
+@implementation FChildEventRegistration
+
+- (id)initWithRepo:(id)repo handle:(FIRDatabaseHandle)fHandle callbacks:(NSDictionary *)callbackBlocks cancelCallback:(fbt_void_nserror)cancelCallbackBlock {
+ self = [super init];
+ if (self) {
+ self.repo = repo;
+ self.handle = fHandle;
+ self.callbacks = callbackBlocks;
+ self.cancelCallback = cancelCallbackBlock;
+ }
+ return self;
+}
+
+- (BOOL) responseTo:(FIRDataEventType)eventType {
+ return self.callbacks != nil && [self.callbacks objectForKey:[NSNumber numberWithInteger:eventType]] != nil;
+}
+
+- (FDataEvent *) createEventFrom:(FChange *)change query:(FQuerySpec *)query {
+ FIRDatabaseReference *ref = [[FIRDatabaseReference alloc] initWithRepo:self.repo path:[query.path childFromString:change.childKey]];
+ FIRDataSnapshot *snapshot = [[FIRDataSnapshot alloc] initWithRef:ref indexedNode:change.indexedNode];
+
+ FDataEvent *eventData = [[FDataEvent alloc] initWithEventType:change.type eventRegistration:self
+ dataSnapshot:snapshot prevName:change.prevKey];
+ return eventData;
+}
+
+- (void) fireEvent:(id <FEvent>)event queue:(dispatch_queue_t)queue {
+ if ([event isCancelEvent]) {
+ FCancelEvent *cancelEvent = event;
+ FFLog(@"I-RDB061001", @"Raising cancel value event on %@", event.path);
+ NSAssert(self.cancelCallback != nil, @"Raising a cancel event on a listener with no cancel callback");
+ dispatch_async(queue, ^{
+ self.cancelCallback(cancelEvent.error);
+ });
+ } else if (self.callbacks != nil) {
+ FDataEvent *dataEvent = event;
+ FFLog(@"I-RDB061002", @"Raising event callback (%ld) on %@", (long)dataEvent.eventType, dataEvent.path);
+ fbt_void_datasnapshot_nsstring callback = [self.callbacks objectForKey:[NSNumber numberWithInteger:dataEvent.eventType]];
+
+ if (callback != nil) {
+ dispatch_async(queue, ^{
+ callback(dataEvent.snapshot, dataEvent.prevName);
+ });
+ }
+ }
+}
+
+- (FCancelEvent *) createCancelEventFromError:(NSError *)error path:(FPath *)path {
+ if (self.cancelCallback != nil) {
+ return [[FCancelEvent alloc] initWithEventRegistration:self error:error path:path];
+ } else {
+ return nil;
+ }
+}
+
+- (BOOL) matches:(id<FEventRegistration>)other {
+ return self.handle == NSNotFound || other.handle == NSNotFound || self.handle == other.handle;
+}
+
+
+@end
diff --git a/Firebase/Database/Core/View/FDataEvent.h b/Firebase/Database/Core/View/FDataEvent.h
new file mode 100644
index 0000000..da90b03
--- /dev/null
+++ b/Firebase/Database/Core/View/FDataEvent.h
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+#import "FIRDataSnapshot.h"
+#import "FIRDatabaseReference.h"
+#import "FTupleUserCallback.h"
+#import "FEvent.h"
+
+@protocol FEventRegistration;
+@protocol FIndex;
+
+@interface FDataEvent : NSObject<FEvent>
+
+- initWithEventType:(FIRDataEventType)type eventRegistration:(id<FEventRegistration>)eventRegistration
+ dataSnapshot:(FIRDataSnapshot *)dataSnapshot;
+- initWithEventType:(FIRDataEventType)type eventRegistration:(id<FEventRegistration>)eventRegistration
+ dataSnapshot:(FIRDataSnapshot *)snapshot prevName:(NSString *)prevName;
+
+
+@property (nonatomic, strong, readonly) id<FEventRegistration> eventRegistration;
+@property (nonatomic, strong, readonly) FIRDataSnapshot * snapshot;
+@property (nonatomic, strong, readonly) NSString* prevName;
+@property (nonatomic, readonly) FIRDataEventType eventType;
+
+@end
diff --git a/Firebase/Database/Core/View/FDataEvent.m b/Firebase/Database/Core/View/FDataEvent.m
new file mode 100644
index 0000000..6c97faf
--- /dev/null
+++ b/Firebase/Database/Core/View/FDataEvent.m
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FDataEvent.h"
+#import "FEventRegistration.h"
+#import "FIndex.h"
+#import "FIRDatabaseQuery_Private.h"
+
+@interface FDataEvent ()
+@property (nonatomic, strong, readwrite) id<FEventRegistration> eventRegistration;
+@property (nonatomic, strong, readwrite) FIRDataSnapshot *snapshot;
+@property (nonatomic, strong, readwrite) NSString *prevName;
+@property (nonatomic, readwrite) FIRDataEventType eventType;
+@end
+
+@implementation FDataEvent
+
+@synthesize eventRegistration;
+@synthesize snapshot;
+@synthesize prevName;
+@synthesize eventType;
+
+- (id)initWithEventType:(FIRDataEventType)type eventRegistration:(id <FEventRegistration>)registration dataSnapshot:(FIRDataSnapshot *)dataSnapshot {
+ return [self initWithEventType:type eventRegistration:registration dataSnapshot:dataSnapshot prevName:nil];
+}
+
+- (id)initWithEventType:(FIRDataEventType)type eventRegistration:(id <FEventRegistration>)registration dataSnapshot:(FIRDataSnapshot *)dataSnapshot prevName:(NSString *)previousName {
+ self = [super init];
+ if (self) {
+ self.eventRegistration = registration;
+ self.snapshot = dataSnapshot;
+ self.prevName = previousName;
+ self.eventType = type;
+ }
+ return self;
+}
+
+- (FPath *) path {
+ // Used for logging, so delay calculation
+ FIRDatabaseReference *ref = self.snapshot.ref;
+ if (self.eventType == FIRDataEventTypeValue) {
+ return ref.path;
+ } else {
+ return ref.parent.path;
+ }
+}
+
+- (void) fireEventOnQueue:(dispatch_queue_t)queue {
+ [self.eventRegistration fireEvent:self queue:queue];
+}
+
+- (BOOL) isCancelEvent {
+ return NO;
+}
+
+
+- (NSString *) description {
+ return [NSString stringWithFormat:@"event %d, data: %@", (int) eventType, [snapshot value]];
+}
+
+@end
diff --git a/Firebase/Database/Core/View/FEvent.h b/Firebase/Database/Core/View/FEvent.h
new file mode 100644
index 0000000..6b9e31a
--- /dev/null
+++ b/Firebase/Database/Core/View/FEvent.h
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+#import "FIRDataEventType.h"
+
+@class FPath;
+
+@protocol FEvent <NSObject>
+- (FPath *) path;
+- (void) fireEventOnQueue:(dispatch_queue_t)queue;
+- (BOOL) isCancelEvent;
+- (NSString *) description;
+@end
diff --git a/Firebase/Database/Core/View/FEventRaiser.h b/Firebase/Database/Core/View/FEventRaiser.h
new file mode 100644
index 0000000..01a0130
--- /dev/null
+++ b/Firebase/Database/Core/View/FEventRaiser.h
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FTypedefs.h"
+
+@class FPath;
+@class FRepo;
+@class FIRDatabaseConfig;
+
+/**
+* Left as instance methods rather than class methods so that we could potentially callback on different queues for different repos.
+* This is semi-parallel to JS's FEventQueue
+*/
+@interface FEventRaiser : NSObject
+
+- (id)initWithQueue:(dispatch_queue_t)queue;
+
+- (void) raiseEvents:(NSArray *)eventDataList;
+- (void) raiseCallback:(fbt_void_void)callback;
+- (void) raiseCallbacks:(NSArray *)callbackList;
+
+@end
diff --git a/Firebase/Database/Core/View/FEventRaiser.m b/Firebase/Database/Core/View/FEventRaiser.m
new file mode 100644
index 0000000..94a0907
--- /dev/null
+++ b/Firebase/Database/Core/View/FEventRaiser.m
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FEventRaiser.h"
+#import "FDataEvent.h"
+#import "FTypedefs.h"
+#import "FUtilities.h"
+#import "FTupleUserCallback.h"
+#import "FRepo.h"
+#import "FRepoManager.h"
+
+@interface FEventRaiser ()
+
+@property (nonatomic, strong) dispatch_queue_t queue;
+
+@end
+
+/**
+* This class exists for symmetry with other clients, but since events are async, we don't need to do the complicated
+* stuff the JS client does to preserve event order.
+*/
+@implementation FEventRaiser
+
+- (id)init {
+ [NSException raise:NSInternalInconsistencyException format:@"Can't use default constructor"];
+ return nil;
+}
+
+- (id)initWithQueue:(dispatch_queue_t)queue {
+ self = [super init];
+ if (self != nil) {
+ self->_queue = queue;
+ }
+ return self;
+}
+
+- (void) raiseEvents:(NSArray *)eventDataList {
+ for (id<FEvent> event in eventDataList) {
+ [event fireEventOnQueue:self.queue];
+ }
+}
+
+- (void) raiseCallback:(fbt_void_void)callback {
+ dispatch_async(self.queue, callback);
+}
+
+- (void) raiseCallbacks:(NSArray *)callbackList {
+ for (fbt_void_void callback in callbackList) {
+ dispatch_async(self.queue, callback);
+ }
+}
+
++ (void) raiseCallbacks:(NSArray *)callbackList queue:(dispatch_queue_t)queue {
+ for (fbt_void_void callback in callbackList) {
+ dispatch_async(queue, callback);
+ }
+}
+
+@end
diff --git a/Firebase/Database/Core/View/FEventRegistration.h b/Firebase/Database/Core/View/FEventRegistration.h
new file mode 100644
index 0000000..5b845ac
--- /dev/null
+++ b/Firebase/Database/Core/View/FEventRegistration.h
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+#import "FChange.h"
+#import "FIRDataEventType.h"
+
+@protocol FEvent;
+@class FDataEvent;
+@class FCancelEvent;
+@class FQuerySpec;
+
+@protocol FEventRegistration <NSObject>
+- (BOOL) responseTo:(FIRDataEventType)eventType;
+- (FDataEvent *) createEventFrom:(FChange *)change query:(FQuerySpec *)query;
+- (void) fireEvent:(id<FEvent>)event queue:(dispatch_queue_t)queue;
+- (FCancelEvent *) createCancelEventFromError:(NSError *)error path:(FPath *)path;
+/**
+* Used to figure out what event registration match the event registration that needs to be removed.
+*/
+- (BOOL) matches:(id<FEventRegistration>)other;
+@property (nonatomic, readonly) FIRDatabaseHandle handle;
+@end
diff --git a/Firebase/Database/Core/View/FKeepSyncedEventRegistration.h b/Firebase/Database/Core/View/FKeepSyncedEventRegistration.h
new file mode 100644
index 0000000..669e012
--- /dev/null
+++ b/Firebase/Database/Core/View/FKeepSyncedEventRegistration.h
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+#import "FEventRegistration.h"
+
+/**
+ * A singleton event registration to mark a query as keep synced
+ */
+@interface FKeepSyncedEventRegistration : NSObject<FEventRegistration>
+
++ (FKeepSyncedEventRegistration *)instance;
+
+@end
diff --git a/Firebase/Database/Core/View/FKeepSyncedEventRegistration.m b/Firebase/Database/Core/View/FKeepSyncedEventRegistration.m
new file mode 100644
index 0000000..806d54f
--- /dev/null
+++ b/Firebase/Database/Core/View/FKeepSyncedEventRegistration.m
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FKeepSyncedEventRegistration.h"
+
+@interface FKeepSyncedEventRegistration ()
+
+@end
+
+@implementation FKeepSyncedEventRegistration
+
++ (FKeepSyncedEventRegistration *)instance {
+ static dispatch_once_t onceToken;
+ static FKeepSyncedEventRegistration *keepSynced;
+ dispatch_once(&onceToken, ^{
+ keepSynced = [[FKeepSyncedEventRegistration alloc] init];
+ });
+ return keepSynced;
+}
+
+- (BOOL) responseTo:(FIRDataEventType)eventType {
+ return NO;
+}
+
+- (FDataEvent *) createEventFrom:(FChange *)change query:(FQuerySpec *)query {
+ [NSException raise:NSInternalInconsistencyException format:@"Should never create event for FKeepSyncedEventRegistration"];
+ return nil;
+}
+
+- (void) fireEvent:(id<FEvent>)event queue:(dispatch_queue_t)queue {
+ [NSException raise:NSInternalInconsistencyException format:@"Should never raise event for FKeepSyncedEventRegistration"];
+}
+
+- (FCancelEvent *) createCancelEventFromError:(NSError *)error path:(FPath *)path {
+ // Don't create cancel events....
+ return nil;
+}
+
+- (FIRDatabaseHandle) handle {
+ // TODO[offline]: returning arbitray, can't return NSNotFound since that is used to match other event registrations
+ // We should really redo this to match on different kind of events (single observer, all observers, cancelled)
+ // rather than on a NSNotFound handle...
+ return NSNotFound - 1;
+}
+
+- (BOOL) matches:(id<FEventRegistration>)other {
+ // Only matches singleton instance
+ return self == other;
+}
+
+@end
diff --git a/Firebase/Database/Core/View/FValueEventRegistration.h b/Firebase/Database/Core/View/FValueEventRegistration.h
new file mode 100644
index 0000000..1220c60
--- /dev/null
+++ b/Firebase/Database/Core/View/FValueEventRegistration.h
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+#import "FEventRegistration.h"
+#import "FTypedefs.h"
+
+@class FRepo;
+
+@interface FValueEventRegistration : NSObject<FEventRegistration>
+
+- (id) initWithRepo:(FRepo *)repo
+ handle:(FIRDatabaseHandle)fHandle
+ callback:(fbt_void_datasnapshot)callbackBlock
+ cancelCallback:(fbt_void_nserror)cancelCallbackBlock;
+
+@property (nonatomic, copy, readonly) fbt_void_datasnapshot callback;
+@property (nonatomic, copy, readonly) fbt_void_nserror cancelCallback;
+@property (nonatomic, readonly) FIRDatabaseHandle handle;
+
+@end
diff --git a/Firebase/Database/Core/View/FValueEventRegistration.m b/Firebase/Database/Core/View/FValueEventRegistration.m
new file mode 100644
index 0000000..d351a4b
--- /dev/null
+++ b/Firebase/Database/Core/View/FValueEventRegistration.m
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FValueEventRegistration.h"
+#import "FIRDatabaseQuery_Private.h"
+#import "FQueryParams.h"
+#import "FQuerySpec.h"
+#import "FIRDataSnapshot_Private.h"
+#import "FCancelEvent.h"
+#import "FDataEvent.h"
+
+@interface FValueEventRegistration ()
+@property (nonatomic, strong) FRepo* repo;
+@property (nonatomic, copy, readwrite) fbt_void_datasnapshot callback;
+@property (nonatomic, copy, readwrite) fbt_void_nserror cancelCallback;
+@property (nonatomic, readwrite) FIRDatabaseHandle handle;
+@end
+
+@implementation FValueEventRegistration
+
+- (id) initWithRepo:(FRepo *)repo
+ handle:(FIRDatabaseHandle)fHandle
+ callback:(fbt_void_datasnapshot)callbackBlock
+ cancelCallback:(fbt_void_nserror)cancelCallbackBlock {
+ self = [super init];
+ if (self) {
+ self.repo = repo;
+ self.handle = fHandle;
+ self.callback = callbackBlock;
+ self.cancelCallback = cancelCallbackBlock;
+ }
+ return self;
+}
+
+- (BOOL) responseTo:(FIRDataEventType)eventType {
+ return eventType == FIRDataEventTypeValue;
+}
+
+- (FDataEvent *) createEventFrom:(FChange *)change query:(FQuerySpec *)query {
+ FIRDatabaseReference *ref = [[FIRDatabaseReference alloc] initWithRepo:self.repo path:query.path];
+ FIRDataSnapshot *snapshot = [[FIRDataSnapshot alloc] initWithRef:ref indexedNode:change.indexedNode];
+ FDataEvent *eventData = [[FDataEvent alloc] initWithEventType:FIRDataEventTypeValue eventRegistration:self
+ dataSnapshot:snapshot];
+ return eventData;
+}
+
+- (void) fireEvent:(id <FEvent>)event queue:(dispatch_queue_t)queue {
+ if ([event isCancelEvent]) {
+ FCancelEvent *cancelEvent = event;
+ FFLog(@"I-RDB065001", @"Raising cancel value event on %@", event.path);
+ NSAssert(self.cancelCallback != nil, @"Raising a cancel event on a listener with no cancel callback");
+ dispatch_async(queue, ^{
+ self.cancelCallback(cancelEvent.error);
+ });
+ } else if (self.callback != nil) {
+ FDataEvent *dataEvent = event;
+ FFLog(@"I-RDB065002", @"Raising value event on %@", dataEvent.snapshot.key);
+ dispatch_async(queue, ^{
+ self.callback(dataEvent.snapshot);
+ });
+ }
+}
+
+- (FCancelEvent *) createCancelEventFromError:(NSError *)error path:(FPath *)path {
+ if (self.cancelCallback != nil) {
+ return [[FCancelEvent alloc] initWithEventRegistration:self error:error path:path];
+ } else {
+ return nil;
+ }
+}
+
+- (BOOL) matches:(id<FEventRegistration>)other {
+ return self.handle == NSNotFound || other.handle == NSNotFound || self.handle == other.handle;
+}
+
+@end
diff --git a/Firebase/Database/Core/View/FView.h b/Firebase/Database/Core/View/FView.h
new file mode 100644
index 0000000..2d0761a
--- /dev/null
+++ b/Firebase/Database/Core/View/FView.h
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+@protocol FNode;
+@protocol FOperation;
+@protocol FEventRegistration;
+@class FWriteTreeRef;
+@class FQuerySpec;
+@class FChange;
+@class FPath;
+@class FViewCache;
+
+@interface FViewOperationResult : NSObject
+
+@property (nonatomic, strong, readonly) NSArray* changes;
+@property (nonatomic, strong, readonly) NSArray* events;
+
+@end
+
+
+@interface FView : NSObject
+
+@property (nonatomic, strong, readonly) FQuerySpec *query;
+
+- (id) initWithQuery:(FQuerySpec *)query initialViewCache:(FViewCache *)initialViewCache;
+
+- (id<FNode>) eventCache;
+- (id<FNode>) serverCache;
+- (id<FNode>) completeServerCacheFor:(FPath*)path;
+- (BOOL) isEmpty;
+
+- (void) addEventRegistration:(id<FEventRegistration>)eventRegistration;
+- (NSArray *) removeEventRegistration:(id<FEventRegistration>)eventRegistration cancelError:(NSError *)cancelError;
+
+- (FViewOperationResult *) applyOperation:(id <FOperation>)operation writesCache:(FWriteTreeRef *)writesCache serverCache:(id <FNode>)optCompleteServerCache;
+- (NSArray *) initialEvents:(id<FEventRegistration>)registration;
+
+@end
diff --git a/Firebase/Database/Core/View/FView.m b/Firebase/Database/Core/View/FView.m
new file mode 100644
index 0000000..1aea4d7
--- /dev/null
+++ b/Firebase/Database/Core/View/FView.m
@@ -0,0 +1,223 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FView.h"
+#import "FNode.h"
+#import "FWriteTreeRef.h"
+#import "FOperation.h"
+#import "FIRDatabaseQuery.h"
+#import "FIRDatabaseQuery_Private.h"
+#import "FEventRegistration.h"
+#import "FQueryParams.h"
+#import "FQuerySpec.h"
+#import "FViewCache.h"
+#import "FPath.h"
+#import "FEventGenerator.h"
+#import "FOperationSource.h"
+#import "FCancelEvent.h"
+#import "FIndexedFilter.h"
+#import "FCacheNode.h"
+#import "FEmptyNode.h"
+#import "FViewProcessor.h"
+#import "FViewProcessorResult.h"
+#import "FIndexedNode.h"
+
+@interface FViewOperationResult ()
+
+@property (nonatomic, strong, readwrite) NSArray *changes;
+@property (nonatomic, strong, readwrite) NSArray *events;
+
+@end
+
+@implementation FViewOperationResult
+
+- (id)initWithChanges:(NSArray *)changes events:(NSArray *)events {
+ self = [super init];
+ if (self != nil) {
+ self->_changes = changes;
+ self->_events = events;
+ }
+ return self;
+}
+
+@end
+
+/**
+* A view represents a specific location and query that has 1 or more event registrations.
+*
+* It does several things:
+* - Maintains the list of event registration for this location/query.
+* - Maintains a cache of the data visible for this location/query.
+* - Applies new operations (via applyOperation), updates the cache, and based on the event
+* registrations returns the set of events to be raised.
+*/
+@interface FView ()
+
+@property (nonatomic, strong, readwrite) FQuerySpec *query;
+@property (nonatomic, strong) FViewProcessor *processor;
+@property (nonatomic, strong) FViewCache *viewCache;
+@property (nonatomic, strong) NSMutableArray *eventRegistrations;
+@property (nonatomic, strong) FEventGenerator *eventGenerator;
+
+@end
+
+@implementation FView
+- (id) initWithQuery:(FQuerySpec *)query initialViewCache:(FViewCache *)initialViewCache {
+ self = [super init];
+ if (self) {
+ self.query = query;
+
+ FIndexedFilter *indexFilter = [[FIndexedFilter alloc] initWithIndex:query.index];
+ id<FNodeFilter> filter = query.params.nodeFilter;
+ self.processor = [[FViewProcessor alloc] initWithFilter:filter];
+ FCacheNode *initialServerCache = initialViewCache.cachedServerSnap;
+ FCacheNode *initialEventCache = initialViewCache.cachedEventSnap;
+
+ // Don't filter server node with other filter than index, wait for tagged listen
+ FIndexedNode *emptyIndexedNode = [FIndexedNode indexedNodeWithNode:[FEmptyNode emptyNode] index:query.index];
+ FIndexedNode *serverSnap = [indexFilter updateFullNode:emptyIndexedNode
+ withNewNode:initialServerCache.indexedNode
+ accumulator:nil];
+ FIndexedNode *eventSnap = [filter updateFullNode:emptyIndexedNode
+ withNewNode:initialEventCache.indexedNode
+ accumulator:nil];
+ FCacheNode *newServerCache = [[FCacheNode alloc] initWithIndexedNode:serverSnap
+ isFullyInitialized:initialServerCache.isFullyInitialized
+ isFiltered:indexFilter.filtersNodes];
+ FCacheNode *newEventCache = [[FCacheNode alloc] initWithIndexedNode:eventSnap
+ isFullyInitialized:initialEventCache.isFullyInitialized
+ isFiltered:filter.filtersNodes];
+
+ self.viewCache = [[FViewCache alloc] initWithEventCache:newEventCache serverCache:newServerCache];
+
+ self.eventRegistrations = [[NSMutableArray alloc] init];
+
+ self.eventGenerator = [[FEventGenerator alloc] initWithQuery:query];
+ }
+
+ return self;
+}
+
+- (id <FNode>) serverCache {
+ return self.viewCache.cachedServerSnap.node;
+}
+
+- (id <FNode>) eventCache {
+ return self.viewCache.cachedEventSnap.node;
+}
+
+- (id <FNode>) completeServerCacheFor:(FPath*)path {
+ id<FNode> cache = self.viewCache.completeServerSnap;
+ if (cache) {
+ // If this isn't a "loadsAllData" view, then cache isn't actually a complete cache and
+ // we need to see if it contains the child we're interested in.
+ if ([self.query loadsAllData] ||
+ (!path.isEmpty && ![cache getImmediateChild:path.getFront].isEmpty)) {
+ return [cache getChild:path];
+ }
+ }
+ return nil;
+}
+
+- (BOOL) isEmpty {
+ return self.eventRegistrations.count == 0;
+}
+
+- (void) addEventRegistration:(id <FEventRegistration>)eventRegistration {
+ [self.eventRegistrations addObject:eventRegistration];
+}
+
+/**
+* @param eventRegistration If null, remove all callbacks.
+* @param cancelError If a cancelError is provided, appropriate cancel events will be returned.
+* @return Cancel events, if cancelError was provided.
+*/
+- (NSArray *) removeEventRegistration:(id <FEventRegistration>)eventRegistration cancelError:(NSError *)cancelError {
+ NSMutableArray *cancelEvents = [[NSMutableArray alloc] init];
+ if (cancelError != nil) {
+ NSAssert(eventRegistration == nil, @"A cancel should cancel all event registrations.");
+ FPath *path = self.query.path;
+ for (id <FEventRegistration> registration in self.eventRegistrations) {
+ FCancelEvent *maybeEvent = [registration createCancelEventFromError:cancelError path:path];
+ if (maybeEvent) {
+ [cancelEvents addObject:maybeEvent];
+ }
+ }
+ }
+
+ if (eventRegistration) {
+ NSUInteger i = 0;
+ while (i < self.eventRegistrations.count) {
+ id<FEventRegistration> existing = self.eventRegistrations[i];
+ if ([existing matches:eventRegistration]) {
+ [self.eventRegistrations removeObjectAtIndex:i];
+ } else {
+ i++;
+ }
+ }
+ } else {
+ [self.eventRegistrations removeAllObjects];
+ }
+ return cancelEvents;
+}
+
+/**
+ * Applies the given Operation, updates our cache, and returns the appropriate events and changes
+ */
+- (FViewOperationResult *) applyOperation:(id <FOperation>)operation writesCache:(FWriteTreeRef *)writesCache serverCache:(id <FNode>)optCompleteServerCache {
+ if (operation.type == FOperationTypeMerge && operation.source.queryParams != nil) {
+ NSAssert(self.viewCache.completeServerSnap != nil, @"We should always have a full cache before handling merges");
+ NSAssert(self.viewCache.completeEventSnap != nil, @"Missing event cache, even though we have a server cache");
+ }
+ FViewCache *oldViewCache = self.viewCache;
+ FViewProcessorResult *result = [self.processor applyOperationOn:oldViewCache operation:operation writesCache:writesCache completeCache:optCompleteServerCache];
+
+ NSAssert(result.viewCache.cachedServerSnap.isFullyInitialized || !oldViewCache.cachedServerSnap.isFullyInitialized, @"Once a server snap is complete, it should never go back.");
+
+ self.viewCache = result.viewCache;
+ NSArray *events = [self generateEventsForChanges:result.changes eventCache:result.viewCache.cachedEventSnap.indexedNode registration:nil];
+ return [[FViewOperationResult alloc] initWithChanges:result.changes events:events];
+}
+
+- (NSArray *) initialEvents:(id<FEventRegistration>)registration {
+ FCacheNode *eventSnap = self.viewCache.cachedEventSnap;
+ NSMutableArray *initialChanges = [[NSMutableArray alloc] init];
+ [eventSnap.indexedNode.node enumerateChildrenUsingBlock:^(NSString *key, id<FNode> node, BOOL *stop) {
+ FIndexedNode *indexed = [FIndexedNode indexedNodeWithNode:node];
+ FChange *change = [[FChange alloc] initWithType:FIRDataEventTypeChildAdded indexedNode:indexed childKey:key];
+ [initialChanges addObject:change];
+ }];
+ if (eventSnap.isFullyInitialized) {
+ FChange *change = [[FChange alloc] initWithType:FIRDataEventTypeValue indexedNode:eventSnap.indexedNode];
+ [initialChanges addObject:change];
+ }
+ return [self generateEventsForChanges:initialChanges eventCache:eventSnap.indexedNode registration:registration];
+}
+
+- (NSArray *) generateEventsForChanges:(NSArray *)changes eventCache:(FIndexedNode *)eventCache registration:(id<FEventRegistration>)registration {
+ NSArray *registrations;
+ if (registration == nil) {
+ registrations = [[NSArray alloc] initWithArray:self.eventRegistrations];
+ } else {
+ registrations = [[NSArray alloc] initWithObjects:registration, nil];
+ }
+ return [self.eventGenerator generateEventsForChanges:changes eventCache:eventCache eventRegistrations:registrations];
+}
+
+- (NSString *) description {
+ return [NSString stringWithFormat:@"FView (%@)", self.query];
+}
+@end
diff --git a/Firebase/Database/Core/View/FViewCache.h b/Firebase/Database/Core/View/FViewCache.h
new file mode 100644
index 0000000..4d01877
--- /dev/null
+++ b/Firebase/Database/Core/View/FViewCache.h
@@ -0,0 +1,35 @@
+#/*
+* Copyright 2017 Google
+*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+*/
+
+#import <Foundation/Foundation.h>
+
+@protocol FNode;
+@class FCacheNode;
+@class FIndexedNode;
+
+@interface FViewCache : NSObject
+
+- (id) initWithEventCache:(FCacheNode *)eventCache serverCache:(FCacheNode *)serverCache;
+
+- (FViewCache *) updateEventSnap:(FIndexedNode *)eventSnap isComplete:(BOOL)complete isFiltered:(BOOL)filtered;
+- (FViewCache *) updateServerSnap:(FIndexedNode *)serverSnap isComplete:(BOOL)complete isFiltered:(BOOL)filtered;
+
+@property (nonatomic, strong, readonly) FCacheNode *cachedEventSnap;
+@property (nonatomic, strong, readonly) id<FNode> completeEventSnap;
+@property (nonatomic, strong, readonly) FCacheNode *cachedServerSnap;
+@property (nonatomic, strong, readonly) id<FNode> completeServerSnap;
+
+@end
diff --git a/Firebase/Database/Core/View/FViewCache.m b/Firebase/Database/Core/View/FViewCache.m
new file mode 100644
index 0000000..c6ec8b1
--- /dev/null
+++ b/Firebase/Database/Core/View/FViewCache.m
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FViewCache.h"
+#import "FCacheNode.h"
+#import "FNode.h"
+#import "FEmptyNode.h"
+
+@interface FViewCache ()
+@property (nonatomic, strong, readwrite) FCacheNode *cachedEventSnap;
+@property (nonatomic, strong, readwrite) FCacheNode *cachedServerSnap;
+@end
+
+@implementation FViewCache
+
+- (id) initWithEventCache:(FCacheNode *)eventCache serverCache:(FCacheNode *)serverCache {
+ self = [super init];
+ if (self) {
+ self.cachedEventSnap = eventCache;
+ self.cachedServerSnap = serverCache;
+ }
+ return self;
+}
+
+- (FViewCache *) updateEventSnap:(FIndexedNode *)eventSnap isComplete:(BOOL)complete isFiltered:(BOOL)filtered {
+ FCacheNode *updatedEventCache = [[FCacheNode alloc] initWithIndexedNode:eventSnap
+ isFullyInitialized:complete
+ isFiltered:filtered];
+ return [[FViewCache alloc] initWithEventCache:updatedEventCache serverCache:self.cachedServerSnap];
+}
+
+- (FViewCache *) updateServerSnap:(FIndexedNode *)serverSnap isComplete:(BOOL)complete isFiltered:(BOOL)filtered {
+ FCacheNode *updatedServerCache = [[FCacheNode alloc] initWithIndexedNode:serverSnap
+ isFullyInitialized:complete
+ isFiltered:filtered];
+ return [[FViewCache alloc] initWithEventCache:self.cachedEventSnap serverCache:updatedServerCache];
+}
+
+- (id<FNode>) completeEventSnap {
+ return (self.cachedEventSnap.isFullyInitialized) ? self.cachedEventSnap.node : nil;
+}
+
+- (id<FNode>) completeServerSnap {
+ return (self.cachedServerSnap.isFullyInitialized) ? self.cachedServerSnap.node : nil;
+}
+
+
+@end
diff --git a/Firebase/Database/Core/View/Filter/FChildChangeAccumulator.h b/Firebase/Database/Core/View/Filter/FChildChangeAccumulator.h
new file mode 100644
index 0000000..59b0a85
--- /dev/null
+++ b/Firebase/Database/Core/View/Filter/FChildChangeAccumulator.h
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+@class FChange;
+
+
+@interface FChildChangeAccumulator : NSObject
+
+- (id) init;
+- (void) trackChildChange:(FChange *)change;
+- (NSArray *) changes;
+
+@end
diff --git a/Firebase/Database/Core/View/Filter/FChildChangeAccumulator.m b/Firebase/Database/Core/View/Filter/FChildChangeAccumulator.m
new file mode 100644
index 0000000..e43fd7c
--- /dev/null
+++ b/Firebase/Database/Core/View/Filter/FChildChangeAccumulator.m
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FChildChangeAccumulator.h"
+#import "FChange.h"
+#import "FIndex.h"
+
+@interface FChildChangeAccumulator ()
+@property (nonatomic, strong) NSMutableDictionary *changeMap;
+@end
+
+@implementation FChildChangeAccumulator
+
+- (id) init {
+ self = [super init];
+ if (self) {
+ self.changeMap = [[NSMutableDictionary alloc] init];
+ }
+ return self;
+}
+
+- (void) trackChildChange:(FChange *)change {
+ FIRDataEventType type = change.type;
+ NSString *childKey = change.childKey;
+ NSAssert(type == FIRDataEventTypeChildAdded || type == FIRDataEventTypeChildChanged || type == FIRDataEventTypeChildRemoved, @"Only child changes supported for tracking.");
+ NSAssert(![change.childKey isEqualToString:@".priority"], @"Changes not tracked on priority");
+ if (self.changeMap[childKey] != nil) {
+ FChange *oldChange = [self.changeMap objectForKey:childKey];
+ FIRDataEventType oldType = oldChange.type;
+ if (type == FIRDataEventTypeChildAdded && oldType == FIRDataEventTypeChildRemoved) {
+ FChange *newChange = [[FChange alloc] initWithType:FIRDataEventTypeChildChanged
+ indexedNode:change.indexedNode
+ childKey:childKey
+ oldIndexedNode:oldChange.indexedNode];
+ [self.changeMap setObject:newChange forKey:childKey];
+ } else if (type == FIRDataEventTypeChildRemoved && oldType == FIRDataEventTypeChildAdded) {
+ [self.changeMap removeObjectForKey:childKey];
+ } else if (type == FIRDataEventTypeChildRemoved && oldType == FIRDataEventTypeChildChanged) {
+ FChange *newChange = [[FChange alloc] initWithType:FIRDataEventTypeChildRemoved
+ indexedNode:oldChange.oldIndexedNode
+ childKey:childKey];
+ [self.changeMap setObject:newChange forKey:childKey];
+ } else if (type == FIRDataEventTypeChildChanged && oldType == FIRDataEventTypeChildAdded) {
+ FChange *newChange = [[FChange alloc] initWithType:FIRDataEventTypeChildAdded
+ indexedNode:change.indexedNode
+ childKey:childKey];
+ [self.changeMap setObject:newChange forKey:childKey];
+ } else if (type == FIRDataEventTypeChildChanged && oldType == FIRDataEventTypeChildChanged) {
+ FChange *newChange = [[FChange alloc] initWithType:FIRDataEventTypeChildChanged
+ indexedNode:change.indexedNode
+ childKey:childKey
+ oldIndexedNode:oldChange.oldIndexedNode];
+ [self.changeMap setObject:newChange forKey:childKey];
+ } else {
+ NSString *reason = [NSString stringWithFormat:@"Illegal combination of changes: %@ occurred after %@", change, oldChange];
+ @throw [[NSException alloc] initWithName:@"FirebaseDatabaseInternalError" reason:reason userInfo:nil];
+ }
+ } else {
+ [self.changeMap setObject:change forKey:childKey];
+ }
+}
+
+- (NSArray *) changes {
+ return [self.changeMap allValues];
+}
+
+@end
diff --git a/Firebase/Database/Core/View/Filter/FCompleteChildSource.h b/Firebase/Database/Core/View/Filter/FCompleteChildSource.h
new file mode 100644
index 0000000..4e99045
--- /dev/null
+++ b/Firebase/Database/Core/View/Filter/FCompleteChildSource.h
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+@protocol FNode;
+@class FNamedNode;
+@protocol FIndex;
+
+@protocol FCompleteChildSource<NSObject>
+
+- (id<FNode>) completeChild:(NSString *)childKey;
+- (FNamedNode *) childByIndex:(id<FIndex>)index afterChild:(FNamedNode *)child isReverse:(BOOL)reverse;
+
+@end
diff --git a/Firebase/Database/Core/View/Filter/FIndexedFilter.h b/Firebase/Database/Core/View/Filter/FIndexedFilter.h
new file mode 100644
index 0000000..5081a77
--- /dev/null
+++ b/Firebase/Database/Core/View/Filter/FIndexedFilter.h
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+#import "FNodeFilter.h"
+
+@protocol FIndex;
+
+
+@interface FIndexedFilter : NSObject<FNodeFilter>
+
+- (id) initWithIndex:(id<FIndex>)theIndex;
+
+@end
diff --git a/Firebase/Database/Core/View/Filter/FIndexedFilter.m b/Firebase/Database/Core/View/Filter/FIndexedFilter.m
new file mode 100644
index 0000000..44c411c
--- /dev/null
+++ b/Firebase/Database/Core/View/Filter/FIndexedFilter.m
@@ -0,0 +1,147 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FNode.h"
+#import "FIndexedFilter.h"
+#import "FChildChangeAccumulator.h"
+#import "FIndex.h"
+#import "FChange.h"
+#import "FChildrenNode.h"
+#import "FKeyIndex.h"
+#import "FEmptyNode.h"
+#import "FIndexedNode.h"
+
+@interface FIndexedFilter ()
+@property (nonatomic, strong, readwrite) id<FIndex> index;
+@end
+
+@implementation FIndexedFilter
+- (id) initWithIndex:(id<FIndex>)theIndex {
+ self = [super init];
+ if (self) {
+ self.index = theIndex;
+ }
+ return self;
+}
+
+- (FIndexedNode *)updateChildIn:(FIndexedNode *)indexedNode
+ forChildKey:(NSString *)childKey
+ newChild:(id<FNode>)newChildSnap
+ affectedPath:(FPath *)affectedPath
+ fromSource:(id<FCompleteChildSource>)source
+ accumulator:(FChildChangeAccumulator *)optChangeAccumulator
+{
+ NSAssert([indexedNode hasIndex:self.index], @"The index in FIndexedNode must match the index of the filter");
+ id<FNode> node = indexedNode.node;
+ id<FNode> oldChildSnap = [node getImmediateChild:childKey];
+
+ // Check if anything actually changed.
+ if ([[oldChildSnap getChild:affectedPath] isEqual:[newChildSnap getChild:affectedPath]]) {
+ // There's an edge case where a child can enter or leave the view because affectedPath was set to null.
+ // In this case, affectedPath will appear null in both the old and new snapshots. So we need
+ // to avoid treating these cases as "nothing changed."
+ if (oldChildSnap.isEmpty == newChildSnap.isEmpty) {
+ // Nothing changed.
+ #ifdef DEBUG
+ NSAssert([oldChildSnap isEqual:newChildSnap], @"Old and new snapshots should be equal.");
+ #endif
+
+ return indexedNode;
+ }
+ }
+ if (optChangeAccumulator) {
+ if (newChildSnap.isEmpty) {
+ if ([node hasChild:childKey]) {
+ FChange *change = [[FChange alloc] initWithType:FIRDataEventTypeChildRemoved
+ indexedNode:[FIndexedNode indexedNodeWithNode:oldChildSnap]
+ childKey:childKey];
+ [optChangeAccumulator trackChildChange:change];
+ } else {
+ NSAssert(node.isLeafNode, @"A child remove without an old child only makes sense on a leaf node.");
+ }
+ } else if (oldChildSnap.isEmpty) {
+ FChange *change = [[FChange alloc] initWithType:FIRDataEventTypeChildAdded
+ indexedNode:[FIndexedNode indexedNodeWithNode:newChildSnap]
+ childKey:childKey];
+ [optChangeAccumulator trackChildChange:change];
+ } else {
+ FChange *change = [[FChange alloc] initWithType:FIRDataEventTypeChildChanged
+ indexedNode:[FIndexedNode indexedNodeWithNode:newChildSnap]
+ childKey:childKey
+ oldIndexedNode:[FIndexedNode indexedNodeWithNode:oldChildSnap]];
+ [optChangeAccumulator trackChildChange:change];
+ }
+ }
+ if (node.isLeafNode && newChildSnap.isEmpty) {
+ return indexedNode;
+ } else {
+ return [indexedNode updateChild:childKey withNewChild:newChildSnap];
+ }
+}
+
+- (FIndexedNode *)updateFullNode:(FIndexedNode *)oldSnap
+ withNewNode:(FIndexedNode *)newSnap
+ accumulator:(FChildChangeAccumulator *)optChangeAccumulator
+{
+ if (optChangeAccumulator) {
+ [oldSnap.node enumerateChildrenUsingBlock:^(NSString *childKey, id<FNode> childNode, BOOL *stop) {
+ if (![newSnap.node hasChild:childKey]) {
+ FChange *change = [[FChange alloc] initWithType:FIRDataEventTypeChildRemoved
+ indexedNode:[FIndexedNode indexedNodeWithNode:childNode]
+ childKey:childKey];
+ [optChangeAccumulator trackChildChange:change];
+ }
+ }];
+
+ [newSnap.node enumerateChildrenUsingBlock:^(NSString *childKey, id<FNode> childNode, BOOL *stop) {
+ if ([oldSnap.node hasChild:childKey]) {
+ id<FNode> oldChildSnap = [oldSnap.node getImmediateChild:childKey];
+ if (![oldChildSnap isEqual:childNode]) {
+ FChange *change = [[FChange alloc] initWithType:FIRDataEventTypeChildChanged
+ indexedNode:[FIndexedNode indexedNodeWithNode:childNode]
+ childKey:childKey
+ oldIndexedNode:[FIndexedNode indexedNodeWithNode:oldChildSnap]];
+ [optChangeAccumulator trackChildChange:change];
+ }
+ } else {
+ FChange *change = [[FChange alloc] initWithType:FIRDataEventTypeChildAdded
+ indexedNode:[FIndexedNode indexedNodeWithNode:childNode]
+ childKey:childKey];
+ [optChangeAccumulator trackChildChange:change];
+ }
+ }];
+ }
+ return newSnap;
+}
+
+- (FIndexedNode *)updatePriority:(id<FNode>)priority forNode:(FIndexedNode *)oldSnap
+{
+ if ([oldSnap.node isEmpty]) {
+ return oldSnap;
+ } else {
+ return [oldSnap updatePriority:priority];
+ }
+}
+
+- (BOOL) filtersNodes {
+ return NO;
+}
+
+- (id<FNodeFilter>) indexedFilter {
+ return self;
+}
+
+@end
diff --git a/Firebase/Database/Core/View/Filter/FLimitedFilter.h b/Firebase/Database/Core/View/Filter/FLimitedFilter.h
new file mode 100644
index 0000000..1690980
--- /dev/null
+++ b/Firebase/Database/Core/View/Filter/FLimitedFilter.h
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+#import "FNodeFilter.h"
+
+@class FQueryParams;
+
+
+@interface FLimitedFilter : NSObject<FNodeFilter>
+
+- (id) initWithQueryParams:(FQueryParams *)params;
+@end
diff --git a/Firebase/Database/Core/View/Filter/FLimitedFilter.m b/Firebase/Database/Core/View/Filter/FLimitedFilter.m
new file mode 100644
index 0000000..8bc6e87
--- /dev/null
+++ b/Firebase/Database/Core/View/Filter/FLimitedFilter.m
@@ -0,0 +1,243 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FLimitedFilter.h"
+#import "FChildChangeAccumulator.h"
+#import "FIndex.h"
+#import "FRangedFilter.h"
+#import "FQueryParams.h"
+#import "FQueryParams.h"
+#import "FNamedNode.h"
+#import "FEmptyNode.h"
+#import "FChildrenNode.h"
+#import "FCompleteChildSource.h"
+#import "FChange.h"
+#import "FTreeSortedDictionary.h"
+
+@interface FLimitedFilter ()
+@property (nonatomic, strong) FRangedFilter *rangedFilter;
+@property (nonatomic, strong, readwrite) id<FIndex> index;
+@property (nonatomic) NSInteger limit;
+@property (nonatomic) BOOL reverse;
+
+@end
+
+@implementation FLimitedFilter
+- (id) initWithQueryParams:(FQueryParams *)params {
+ self = [super init];
+ if (self) {
+ self.rangedFilter = [[FRangedFilter alloc] initWithQueryParams:params];
+ self.index = params.index;
+ self.limit = params.limit;
+ self.reverse = !params.isViewFromLeft;
+ }
+ return self;
+}
+
+
+- (FIndexedNode *)updateChildIn:(FIndexedNode *)oldSnap
+ forChildKey:(NSString *)childKey
+ newChild:(id<FNode>)newChildSnap
+ affectedPath:(FPath *)affectedPath
+ fromSource:(id<FCompleteChildSource>)source
+ accumulator:(FChildChangeAccumulator *)optChangeAccumulator
+{
+ if (![self.rangedFilter matchesKey:childKey andNode:newChildSnap]) {
+ newChildSnap = [FEmptyNode emptyNode];
+ }
+ if ([[oldSnap.node getImmediateChild:childKey] isEqual:newChildSnap]) {
+ // No change
+ return oldSnap;
+ } else if (oldSnap.node.numChildren < self.limit) {
+ return [[self.rangedFilter indexedFilter] updateChildIn:oldSnap
+ forChildKey:childKey
+ newChild:newChildSnap
+ affectedPath:affectedPath
+ fromSource:source
+ accumulator:optChangeAccumulator];
+ } else {
+ return [self fullLimitUpdateNode:oldSnap
+ forChildKey:childKey
+ newChild:newChildSnap
+ fromSource:source
+ accumulator:optChangeAccumulator];
+ }
+}
+
+- (FIndexedNode *)fullLimitUpdateNode:(FIndexedNode *)oldIndexed
+ forChildKey:(NSString *)childKey
+ newChild:(id<FNode>)newChildSnap
+ fromSource:(id<FCompleteChildSource>)source
+ accumulator:(FChildChangeAccumulator *)optChangeAccumulator
+{
+ NSAssert(oldIndexed.node.numChildren == self.limit, @"Should have number of children equal to limit.");
+
+ FNamedNode *windowBoundary = self.reverse ? oldIndexed.firstChild : oldIndexed.lastChild;
+
+ BOOL inRange = [self.rangedFilter matchesKey:childKey andNode:newChildSnap];
+ if ([oldIndexed.node hasChild:childKey]) {
+ // `childKey` was already in `oldSnap`. Figure out if it remains in the window or needs to be replaced.
+ id<FNode> oldChildSnap = [oldIndexed.node getImmediateChild:childKey];
+
+ // In case the `newChildSnap` falls outside the window, get the `nextChild` that might replace it.
+ FNamedNode *nextChild = [source childByIndex:self.index afterChild:windowBoundary isReverse:(BOOL)self.reverse];
+ if (nextChild != nil && ([nextChild.name isEqualToString:childKey] ||
+ [oldIndexed.node hasChild:nextChild.name])) {
+ // There is a weird edge case where a node is updated as part of a merge in the write tree, but hasn't
+ // been applied to the limited filter yet. Ignore this next child which will be updated later in
+ // the limited filter...
+ nextChild = [source childByIndex:self.index afterChild:nextChild isReverse:self.reverse];
+ }
+
+
+
+ // Figure out if `newChildSnap` is in range and ordered before `nextChild`
+ BOOL remainsInWindow = inRange && !newChildSnap.isEmpty;
+ remainsInWindow = remainsInWindow && (!nextChild || [self.index compareKey:nextChild.name
+ andNode:nextChild.node
+ toOtherKey:childKey
+ andNode:newChildSnap
+ reverse:self.reverse] >= NSOrderedSame);
+ if (remainsInWindow) {
+ // `newChildSnap` is ordered before `nextChild`, so it's a child changed event
+ if (optChangeAccumulator != nil) {
+ FChange *change = [[FChange alloc] initWithType:FIRDataEventTypeChildChanged
+ indexedNode:[FIndexedNode indexedNodeWithNode:newChildSnap]
+ childKey:childKey
+ oldIndexedNode:[FIndexedNode indexedNodeWithNode:oldChildSnap]];
+ [optChangeAccumulator trackChildChange:change];
+ }
+ return [oldIndexed updateChild:childKey withNewChild:newChildSnap];
+ } else {
+ // `newChildSnap` is ordered after `nextChild`, so it's a child removed event
+ if (optChangeAccumulator != nil) {
+ FChange *change = [[FChange alloc] initWithType:FIRDataEventTypeChildRemoved
+ indexedNode:[FIndexedNode indexedNodeWithNode:oldChildSnap]
+ childKey:childKey];
+ [optChangeAccumulator trackChildChange:change];
+ }
+ FIndexedNode *newIndexed = [oldIndexed updateChild:childKey withNewChild:[FEmptyNode emptyNode]];
+
+ // We need to check if the `nextChild` is actually in range before adding it
+ BOOL nextChildInRange = (nextChild != nil) && [self.rangedFilter matchesKey:nextChild.name
+ andNode:nextChild.node];
+ if (nextChildInRange) {
+ if (optChangeAccumulator != nil) {
+ FChange *change = [[FChange alloc] initWithType:FIRDataEventTypeChildAdded
+ indexedNode:[FIndexedNode indexedNodeWithNode:nextChild.node]
+ childKey:nextChild.name];
+ [optChangeAccumulator trackChildChange:change];
+ }
+ return [newIndexed updateChild:nextChild.name withNewChild:nextChild.node];
+ } else {
+ return newIndexed;
+ }
+ }
+ } else if (newChildSnap.isEmpty) {
+ // We're deleting a node, but it was not in the window, so ignore it.
+ return oldIndexed;
+ } else if (inRange) {
+ // `newChildSnap` is in range, but was ordered after `windowBoundary`. If this has changed, we bump out the
+ // `windowBoundary` and add the `newChildSnap`
+ if ([self.index compareKey:windowBoundary.name
+ andNode:windowBoundary.node
+ toOtherKey:childKey
+ andNode:newChildSnap
+ reverse:self.reverse] >= NSOrderedSame) {
+ if (optChangeAccumulator != nil) {
+ FChange *removedChange = [[FChange alloc] initWithType:FIRDataEventTypeChildRemoved
+ indexedNode:[FIndexedNode indexedNodeWithNode:windowBoundary.node]
+ childKey:windowBoundary.name];
+ FChange *addedChange = [[FChange alloc] initWithType:FIRDataEventTypeChildAdded
+ indexedNode:[FIndexedNode indexedNodeWithNode:newChildSnap]
+ childKey:childKey];
+ [optChangeAccumulator trackChildChange:removedChange];
+ [optChangeAccumulator trackChildChange:addedChange];
+ }
+ return [[oldIndexed updateChild:childKey withNewChild:newChildSnap] updateChild:windowBoundary.name
+ withNewChild:[FEmptyNode emptyNode]];
+ } else {
+ return oldIndexed;
+ }
+ } else {
+ // `newChildSnap` was not in range and remains not in range, so ignore it.
+ return oldIndexed;
+ }
+}
+
+- (FIndexedNode *)updateFullNode:(FIndexedNode *)oldSnap
+ withNewNode:(FIndexedNode *)newSnap
+ accumulator:(FChildChangeAccumulator *)optChangeAccumulator
+{
+ __block FIndexedNode *filtered;
+ if (newSnap.node.isLeafNode || newSnap.node.isEmpty) {
+ // Make sure we have a children node with the correct index, not a leaf node
+ filtered = [FIndexedNode indexedNodeWithNode:[FEmptyNode emptyNode] index:self.index];
+ } else {
+ filtered = newSnap;
+ // Don't support priorities on queries.
+ filtered = [filtered updatePriority:[FEmptyNode emptyNode]];
+ FNamedNode *startPost = nil;
+ FNamedNode *endPost = nil;
+ if (self.reverse) {
+ startPost = self.rangedFilter.endPost;
+ endPost = self.rangedFilter.startPost;
+ } else {
+ startPost = self.rangedFilter.startPost;
+ endPost = self.rangedFilter.endPost;
+ }
+ __block BOOL foundStartPost = NO;
+ __block NSUInteger count = 0;
+ [newSnap enumerateChildrenReverse:self.reverse usingBlock:^(NSString *childKey, id<FNode> childNode, BOOL *stop) {
+ if (!foundStartPost && [self.index compareKey:startPost.name
+ andNode:startPost.node
+ toOtherKey:childKey
+ andNode:childNode
+ reverse:self.reverse] <= NSOrderedSame) {
+ // Start adding
+ foundStartPost = YES;
+ }
+ BOOL inRange = foundStartPost && count < self.limit;
+ inRange = inRange && [self.index compareKey:childKey
+ andNode:childNode
+ toOtherKey:endPost.name
+ andNode:endPost.node
+ reverse:self.reverse] <= NSOrderedSame;
+ if (inRange) {
+ count++;
+ } else {
+ filtered = [filtered updateChild:childKey withNewChild:[FEmptyNode emptyNode]];
+ }
+ }];
+ }
+ return [self.indexedFilter updateFullNode:oldSnap withNewNode:filtered accumulator:optChangeAccumulator];
+}
+
+- (FIndexedNode *)updatePriority:(id<FNode>)priority forNode:(FIndexedNode *)oldSnap
+{
+ // Don't support priorities on queries.
+ return oldSnap;
+}
+
+- (BOOL) filtersNodes {
+ return YES;
+}
+
+- (id<FNodeFilter>) indexedFilter {
+ return self.rangedFilter.indexedFilter;
+}
+
+@end
diff --git a/Firebase/Database/Core/View/Filter/FNodeFilter.h b/Firebase/Database/Core/View/Filter/FNodeFilter.h
new file mode 100644
index 0000000..f29a85a
--- /dev/null
+++ b/Firebase/Database/Core/View/Filter/FNodeFilter.h
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+@protocol FNode;
+@class FIndexedNode;
+@protocol FCompleteChildSource;
+@class FChildChangeAccumulator;
+@protocol FIndex;
+@class FPath;
+
+/**
+* FNodeFilter is used to update nodes and complete children of nodes while applying queries on the fly and keeping
+* track of any child changes. This class does not track value changes as value changes depend on more than just the
+* node itself. Different kind of queries require different kind of implementations of this interface.
+*/
+@protocol FNodeFilter<NSObject>
+
+/**
+* Update a single complete child in the snap. If the child equals the old child in the snap, this is a no-op.
+* The method expects an indexed snap.
+*/
+- (FIndexedNode *) updateChildIn:(FIndexedNode *)oldSnap
+ forChildKey:(NSString *)childKey
+ newChild:(id<FNode>)newChildSnap
+ affectedPath:(FPath *)affectedPath
+ fromSource:(id<FCompleteChildSource>)source
+ accumulator:(FChildChangeAccumulator *)optChangeAccumulator;
+
+/**
+* Update a node in full and output any resulting change from this complete update.
+*/
+- (FIndexedNode *) updateFullNode:(FIndexedNode *)oldSnap
+ withNewNode:(FIndexedNode *)newSnap
+ accumulator:(FChildChangeAccumulator *)optChangeAccumulator;
+
+/**
+* Update the priority of the root node
+*/
+- (FIndexedNode *) updatePriority:(id<FNode>)priority forNode:(FIndexedNode *)oldSnap;
+
+/**
+* Returns true if children might be filtered due to query critiera
+*/
+- (BOOL) filtersNodes;
+
+/**
+* Returns the index filter that this filter uses to get a NodeFilter that doesn't filter any children.
+*/
+@property (nonatomic, strong, readonly) id<FNodeFilter> indexedFilter;
+
+/**
+* Returns the index that this filter uses
+*/
+@property (nonatomic, strong, readonly) id<FIndex> index;
+
+@end
diff --git a/Firebase/Database/FClock.h b/Firebase/Database/FClock.h
new file mode 100644
index 0000000..1924ad4
--- /dev/null
+++ b/Firebase/Database/FClock.h
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+@protocol FClock <NSObject>
+
+- (NSTimeInterval)currentTime;
+
+@end
+
+@interface FSystemClock : NSObject<FClock>
+
++ (FSystemClock *)clock;
+
+@end
+
+@interface FOffsetClock : NSObject<FClock>
+
+- (id)initWithClock:(id<FClock>)clock offset:(NSTimeInterval)offset;
+
+@end
diff --git a/Firebase/Database/FClock.m b/Firebase/Database/FClock.m
new file mode 100644
index 0000000..2464056
--- /dev/null
+++ b/Firebase/Database/FClock.m
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FClock.h"
+
+@implementation FSystemClock
+
+- (NSTimeInterval)currentTime {
+ return [[NSDate date] timeIntervalSince1970];
+}
+
++ (FSystemClock *)clock {
+ static dispatch_once_t onceToken;
+ static FSystemClock *clock;
+ dispatch_once(&onceToken, ^{
+ clock = [[FSystemClock alloc] init];
+ });
+ return clock;
+}
+
+@end
+
+@interface FOffsetClock ()
+
+@property (nonatomic, strong) id<FClock> clock;
+@property (nonatomic) NSTimeInterval offset;
+
+@end
+
+@implementation FOffsetClock
+
+- (NSTimeInterval)currentTime {
+ return [self.clock currentTime] + self.offset;
+}
+
+- (id)initWithClock:(id<FClock>)clock offset:(NSTimeInterval)offset {
+ self = [super init];
+ if (self != nil) {
+ self->_clock = clock;
+ self->_offset = offset;
+ }
+ return self;
+}
+
+@end
diff --git a/Firebase/Database/FEventGenerator.h b/Firebase/Database/FEventGenerator.h
new file mode 100644
index 0000000..1bc011b
--- /dev/null
+++ b/Firebase/Database/FEventGenerator.h
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+@class FQuerySpec;
+@class FIndexedNode;
+@protocol FNode;
+
+@interface FEventGenerator : NSObject
+- (id) initWithQuery:(FQuerySpec *)query;
+- (NSArray*) generateEventsForChanges:(NSArray*)changes eventCache:(FIndexedNode *)eventCache
+ eventRegistrations:(NSArray*)registrations;
+@end
diff --git a/Firebase/Database/FEventGenerator.m b/Firebase/Database/FEventGenerator.m
new file mode 100644
index 0000000..f6e8f47
--- /dev/null
+++ b/Firebase/Database/FEventGenerator.m
@@ -0,0 +1,141 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FEventGenerator.h"
+#import "FNode.h"
+#import "FIRDatabaseQuery_Private.h"
+#import "FQueryParams.h"
+#import "FQuerySpec.h"
+#import "FChange.h"
+#import "FNamedNode.h"
+#import "FEventRegistration.h"
+#import "FEvent.h"
+#import "FDataEvent.h"
+
+@interface FEventGenerator ()
+@property (nonatomic, strong) FQuerySpec *query;
+@end
+
+/**
+* An EventGenerator is used to convert "raw" changes (fb.core.view.Change) as computed by the
+* CacheDiffer into actual events (fb.core.view.Event) that can be raised. See generateEventsForChanges()
+* for details.
+*/
+@implementation FEventGenerator
+
+- (id)initWithQuery:(FQuerySpec *)query {
+ self = [super init];
+ if (self) {
+ self.query = query;
+ }
+ return self;
+}
+
+/**
+* Given a set of raw changes (no moved events, and prevName not specified yet), and a set of EventRegistrations that
+* should be notified of these changes, generate the actual events to be raised.
+*
+* Notes:
+* - child_moved events will be synthesized at this time for any child_changed events that affect our index
+* - prevName will be calculated based on the index ordering
+*
+* @param changes NSArray of FChange, not necessarily in order.
+* @param registrations is NSArray of FEventRegistration.
+* @return NSArray of FEvent.
+*/
+- (NSArray *) generateEventsForChanges:(NSArray *)changes
+ eventCache:(FIndexedNode *)eventCache
+ eventRegistrations:(NSArray *)registrations
+{
+ NSMutableArray *events = [[NSMutableArray alloc] init];
+
+ // child_moved is index-specific, so check all our child_changed events to see if we need to materialize
+ // child_moved events with this view's index
+ NSMutableArray *moves = [[NSMutableArray alloc] init];
+ for (FChange *change in changes) {
+ if (change.type == FIRDataEventTypeChildChanged && [self.query.index indexedValueChangedBetween:change.oldIndexedNode.node
+ and:change.indexedNode.node]) {
+ FChange *moveChange = [[FChange alloc] initWithType:FIRDataEventTypeChildMoved
+ indexedNode:change.indexedNode
+ childKey:change.childKey
+ oldIndexedNode:nil];
+ [moves addObject:moveChange];
+ }
+ }
+
+ [self generateEvents:events forType:FIRDataEventTypeChildRemoved changes:changes eventCache:eventCache eventRegistrations:registrations];
+ [self generateEvents:events forType:FIRDataEventTypeChildAdded changes:changes eventCache:eventCache eventRegistrations:registrations];
+ [self generateEvents:events forType:FIRDataEventTypeChildMoved changes:moves eventCache:eventCache eventRegistrations:registrations];
+ [self generateEvents:events forType:FIRDataEventTypeChildChanged changes:changes eventCache:eventCache eventRegistrations:registrations];
+ [self generateEvents:events forType:FIRDataEventTypeValue changes:changes eventCache:eventCache eventRegistrations:registrations];
+
+ return events;
+}
+
+- (void) generateEvents:(NSMutableArray *)events
+ forType:(FIRDataEventType)eventType
+ changes:(NSArray *)changes
+ eventCache:(FIndexedNode *)eventCache
+ eventRegistrations:(NSArray *)registrations
+{
+ NSMutableArray *filteredChanges = [[NSMutableArray alloc] init];
+ for (FChange *change in changes) {
+ if (change.type == eventType) {
+ [filteredChanges addObject:change];
+ }
+ }
+
+ id<FIndex> index = self.query.index;
+
+ [filteredChanges sortUsingComparator:^NSComparisonResult(FChange *one, FChange *two) {
+ if (one.childKey == nil || two.childKey == nil) {
+ @throw [[NSException alloc] initWithName:@"InternalInconsistencyError"
+ reason:@"Should only compare child_ events"
+ userInfo:nil];
+ }
+ return [index compareKey:one.childKey
+ andNode:one.indexedNode.node
+ toOtherKey:two.childKey
+ andNode:two.indexedNode.node];
+ }];
+
+ for (FChange *change in filteredChanges) {
+ for (id<FEventRegistration> registration in registrations) {
+ if ([registration responseTo:eventType]) {
+ id<FEvent> event = [self generateEventForChange:change registration:registration eventCache:eventCache];
+ [events addObject:event];
+ }
+ }
+ }
+}
+
+- (id<FEvent>) generateEventForChange:(FChange *)change
+ registration:(id<FEventRegistration>)registration
+ eventCache:(FIndexedNode *)eventCache
+{
+ FChange *materializedChange;
+ if (change.type == FIRDataEventTypeValue || change.type == FIRDataEventTypeChildRemoved) {
+ materializedChange = change;
+ } else {
+ NSString *prevChildKey = [eventCache predecessorForChildKey:change.childKey
+ childNode:change.indexedNode.node
+ index:self.query.index];
+ materializedChange = [change changeWithPrevKey:prevChildKey];
+ }
+ return [registration createEventFrom:materializedChange query:self.query];
+}
+
+@end
diff --git a/Firebase/Database/FIRDatabaseConfig_Private.h b/Firebase/Database/FIRDatabaseConfig_Private.h
new file mode 100644
index 0000000..b0a9dc4
--- /dev/null
+++ b/Firebase/Database/FIRDatabaseConfig_Private.h
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRDatabaseConfig.h"
+#import "FAuthTokenProvider.h"
+
+@protocol FStorageEngine;
+
+@interface FIRDatabaseConfig ()
+
+@property (nonatomic, readonly) BOOL isFrozen;
+@property (nonatomic, strong, readonly) NSString *sessionIdentifier;
+@property (nonatomic, strong) id<FAuthTokenProvider> authTokenProvider;
+@property (nonatomic, strong) id<FStorageEngine> forceStorageEngine;
+
+- (void)freeze;
+
++ (FIRDatabaseConfig *)configForName:(NSString *)name;
+
++ (FIRDatabaseConfig *)defaultConfig;
+
+@end
diff --git a/Firebase/Database/FIRDatabaseReference.h b/Firebase/Database/FIRDatabaseReference.h
new file mode 100644
index 0000000..eb3fecd
--- /dev/null
+++ b/Firebase/Database/FIRDatabaseReference.h
@@ -0,0 +1,719 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+#import "FIRDatabaseQuery.h"
+#import "FIRDatabase.h"
+#import "FIRDatabaseSwiftNameSupport.h"
+#import "FIRDataSnapshot.h"
+#import "FIRMutableData.h"
+#import "FIRTransactionResult.h"
+#import "FIRServerValue.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@class FIRDatabase;
+
+/**
+ * A FIRDatabaseReference represents a particular location in your Firebase Database
+ * and can be used for reading or writing data to that Firebase Database location.
+ *
+ * This class is the starting point for all Firebase Database operations. After you've
+ * obtained your first FIRDatabaseReference via [FIRDatabase reference], you can use it
+ * to read data (ie. observeEventType:withBlock:), write data (ie. setValue:), and to
+ * create new FIRDatabaseReferences (ie. child:).
+ */
+FIR_SWIFT_NAME(DatabaseReference)
+@interface FIRDatabaseReference : FIRDatabaseQuery
+
+
+#pragma mark - Getting references to children locations
+
+/**
+ * Gets a FIRDatabaseReference for the location at the specified relative path.
+ * The relative path can either be a simple child key (e.g. 'fred') or a
+ * deeper slash-separated path (e.g. 'fred/name/first').
+ *
+ * @param pathString A relative path from this location to the desired child location.
+ * @return A FIRDatabaseReference for the specified relative path.
+ */
+- (FIRDatabaseReference *)child:(NSString *)pathString;
+
+/**
+ * childByAppendingPath: is deprecated, use child: instead.
+ */
+- (FIRDatabaseReference *)childByAppendingPath:(NSString *)pathString __deprecated_msg("use child: instead");
+
+/**
+ * childByAutoId generates a new child location using a unique key and returns a
+ * FIRDatabaseReference to it. This is useful when the children of a Firebase Database
+ * location represent a list of items.
+ *
+ * The unique key generated by childByAutoId: is prefixed with a client-generated
+ * timestamp so that the resulting list will be chronologically-sorted.
+ *
+ * @return A FIRDatabaseReference for the generated location.
+ */
+- (FIRDatabaseReference *) childByAutoId;
+
+
+#pragma mark - Writing data
+
+/** Write data to this Firebase Database location.
+
+This will overwrite any data at this location and all child locations.
+
+Data types that can be set are:
+
+- NSString -- @"Hello World"
+- NSNumber (also includes boolean) -- @YES, @43, @4.333
+- NSDictionary -- @{@"key": @"value", @"nested": @{@"another": @"value"} }
+- NSArray
+
+The effect of the write will be visible immediately and the corresponding
+events will be triggered. Synchronization of the data to the Firebase Database
+servers will also be started.
+
+Passing null for the new value is equivalent to calling remove:;
+all data at this location or any child location will be deleted.
+
+Note that setValue: will remove any priority stored at this location, so if priority
+is meant to be preserved, you should use setValue:andPriority: instead.
+
+@param value The value to be written.
+ */
+- (void) setValue:(nullable id)value;
+
+
+/**
+ * The same as setValue: with a block that gets triggered after the write operation has
+ * been committed to the Firebase Database servers.
+ *
+ * @param value The value to be written.
+ * @param block The block to be called after the write has been committed to the Firebase Database servers.
+ */
+- (void) setValue:(nullable id)value withCompletionBlock:(void (^)(NSError *__nullable error, FIRDatabaseReference * ref))block;
+
+
+/**
+ * The same as setValue: with an additional priority to be attached to the data being written.
+ * Priorities are used to order items.
+ *
+ * @param value The value to be written.
+ * @param priority The priority to be attached to that data.
+ */
+- (void) setValue:(nullable id)value andPriority:(nullable id)priority;
+
+
+/**
+ * The same as setValue:andPriority: with a block that gets triggered after the write operation has
+ * been committed to the Firebase Database servers.
+ *
+ * @param value The value to be written.
+ * @param priority The priority to be attached to that data.
+ * @param block The block to be called after the write has been committed to the Firebase Database servers.
+ */
+- (void) setValue:(nullable id)value andPriority:(nullable id)priority withCompletionBlock:(void (^)(NSError *__nullable error, FIRDatabaseReference * ref))block;
+
+
+/**
+ * Remove the data at this Firebase Database location. Any data at child locations will also be deleted.
+ *
+ * The effect of the delete will be visible immediately and the corresponding events
+ * will be triggered. Synchronization of the delete to the Firebase Database servers will
+ * also be started.
+ *
+ * remove: is equivalent to calling setValue:nil
+ */
+- (void) removeValue;
+
+
+/**
+ * The same as remove: with a block that gets triggered after the remove operation has
+ * been committed to the Firebase Database servers.
+ *
+ * @param block The block to be called after the remove has been committed to the Firebase Database servers.
+ */
+- (void) removeValueWithCompletionBlock:(void (^)(NSError *__nullable error, FIRDatabaseReference * ref))block;
+
+/**
+ * Sets a priority for the data at this Firebase Database location.
+ * Priorities can be used to provide a custom ordering for the children at a location
+ * (if no priorities are specified, the children are ordered by key).
+ *
+ * You cannot set a priority on an empty location. For this reason
+ * setValue:andPriority: should be used when setting initial data with a specific priority
+ * and setPriority: should be used when updating the priority of existing data.
+ *
+ * Children are sorted based on this priority using the following rules:
+ *
+ * Children with no priority come first.
+ * Children with a number as their priority come next. They are sorted numerically by priority (small to large).
+ * Children with a string as their priority come last. They are sorted lexicographically by priority.
+ * Whenever two children have the same priority (including no priority), they are sorted by key. Numeric
+ * keys come first (sorted numerically), followed by the remaining keys (sorted lexicographically).
+ *
+ * Note that priorities are parsed and ordered as IEEE 754 double-precision floating-point numbers.
+ * Keys are always stored as strings and are treated as numbers only when they can be parsed as a
+ * 32-bit integer
+ *
+ * @param priority The priority to set at the specified location.
+ */
+- (void) setPriority:(nullable id)priority;
+
+
+/**
+ * The same as setPriority: with a block that is called once the priority has
+ * been committed to the Firebase Database servers.
+ *
+ * @param priority The priority to set at the specified location.
+ * @param block The block that is triggered after the priority has been written on the servers.
+ */
+- (void) setPriority:(nullable id)priority withCompletionBlock:(void (^)(NSError *__nullable error, FIRDatabaseReference * ref))block;
+
+/**
+ * Updates the values at the specified paths in the dictionary without overwriting other
+ * keys at this location.
+ *
+ * @param values A dictionary of the keys to change and their new values
+ */
+- (void) updateChildValues:(NSDictionary *)values;
+
+/**
+ * The same as update: with a block that is called once the update has been committed to the
+ * Firebase Database servers
+ *
+ * @param values A dictionary of the keys to change and their new values
+ * @param block The block that is triggered after the update has been written on the Firebase Database servers
+ */
+- (void) updateChildValues:(NSDictionary *)values withCompletionBlock:(void (^)(NSError *__nullable error, FIRDatabaseReference * ref))block;
+
+
+#pragma mark - Attaching observers to read data
+
+/**
+ * observeEventType:withBlock: is used to listen for data changes at a particular location.
+ * This is the primary way to read data from the Firebase Database. Your block will be triggered
+ * for the initial data and again whenever the data changes.
+ *
+ * Use removeObserverWithHandle: to stop receiving updates.
+ * @param eventType The type of event to listen for.
+ * @param block The block that should be called with initial data and updates. It is passed the data as a FIRDataSnapshot.
+ * @return A handle used to unregister this block later using removeObserverWithHandle:
+ */
+- (FIRDatabaseHandle)observeEventType:(FIRDataEventType)eventType withBlock:(void (^)(FIRDataSnapshot *snapshot))block;
+
+
+/**
+ * observeEventType:andPreviousSiblingKeyWithBlock: is used to listen for data changes at a particular location.
+ * This is the primary way to read data from the Firebase Database. Your block will be triggered
+ * for the initial data and again whenever the data changes. In addition, for FIRDataEventTypeChildAdded, FIRDataEventTypeChildMoved, and
+ * FIRDataEventTypeChildChanged events, your block will be passed the key of the previous node by priority order.
+ *
+ * Use removeObserverWithHandle: to stop receiving updates.
+ *
+ * @param eventType The type of event to listen for.
+ * @param block The block that should be called with initial data and updates. It is passed the data as a FIRDataSnapshot
+ * and the previous child's key.
+ * @return A handle used to unregister this block later using removeObserverWithHandle:
+ */
+- (FIRDatabaseHandle)observeEventType:(FIRDataEventType)eventType andPreviousSiblingKeyWithBlock:(void (^)(FIRDataSnapshot *snapshot, NSString *__nullable prevKey))block;
+
+
+/**
+ * observeEventType:withBlock: is used to listen for data changes at a particular location.
+ * This is the primary way to read data from the Firebase Database. Your block will be triggered
+ * for the initial data and again whenever the data changes.
+ *
+ * The cancelBlock will be called if you will no longer receive new events due to no longer having permission.
+ *
+ * Use removeObserverWithHandle: to stop receiving updates.
+ *
+ * @param eventType The type of event to listen for.
+ * @param block The block that should be called with initial data and updates. It is passed the data as a FIRDataSnapshot.
+ * @param cancelBlock The block that should be called if this client no longer has permission to receive these events
+ * @return A handle used to unregister this block later using removeObserverWithHandle:
+ */
+- (FIRDatabaseHandle)observeEventType:(FIRDataEventType)eventType withBlock:(void (^)(FIRDataSnapshot *snapshot))block withCancelBlock:(nullable void (^)(NSError* error))cancelBlock;
+
+
+/**
+ * observeEventType:andPreviousSiblingKeyWithBlock: is used to listen for data changes at a particular location.
+ * This is the primary way to read data from the Firebase Database. Your block will be triggered
+ * for the initial data and again whenever the data changes. In addition, for FIRDataEventTypeChildAdded, FIRDataEventTypeChildMoved, and
+ * FIRDataEventTypeChildChanged events, your block will be passed the key of the previous node by priority order.
+ *
+ * The cancelBlock will be called if you will no longer receive new events due to no longer having permission.
+ *
+ * Use removeObserverWithHandle: to stop receiving updates.
+ *
+ * @param eventType The type of event to listen for.
+ * @param block The block that should be called with initial data and updates. It is passed the data as a FIRDataSnapshot
+ * and the previous child's key.
+ * @param cancelBlock The block that should be called if this client no longer has permission to receive these events
+ * @return A handle used to unregister this block later using removeObserverWithHandle:
+ */
+- (FIRDatabaseHandle)observeEventType:(FIRDataEventType)eventType andPreviousSiblingKeyWithBlock:(void (^)(FIRDataSnapshot *snapshot, NSString *__nullable prevKey))block withCancelBlock:(nullable void (^)(NSError* error))cancelBlock;
+
+
+/**
+ * This is equivalent to observeEventType:withBlock:, except the block is immediately canceled after the initial data is returned.
+ *
+ * @param eventType The type of event to listen for.
+ * @param block The block that should be called. It is passed the data as a FIRDataSnapshot.
+ */
+- (void)observeSingleEventOfType:(FIRDataEventType)eventType withBlock:(void (^)(FIRDataSnapshot *snapshot))block;
+
+
+/**
+ * This is equivalent to observeEventType:withBlock:, except the block is immediately canceled after the initial data is returned. In addition, for FIRDataEventTypeChildAdded, FIRDataEventTypeChildMoved, and
+ * FIRDataEventTypeChildChanged events, your block will be passed the key of the previous node by priority order.
+ *
+ * @param eventType The type of event to listen for.
+ * @param block The block that should be called. It is passed the data as a FIRDataSnapshot and the previous child's key.
+ */
+- (void)observeSingleEventOfType:(FIRDataEventType)eventType andPreviousSiblingKeyWithBlock:(void (^)(FIRDataSnapshot *snapshot, NSString *__nullable prevKey))block;
+
+
+/**
+ * This is equivalent to observeEventType:withBlock:, except the block is immediately canceled after the initial data is returned.
+ *
+ * The cancelBlock will be called if you do not have permission to read data at this location.
+ *
+ * @param eventType The type of event to listen for.
+ * @param block The block that should be called. It is passed the data as a FIRDataSnapshot.
+ * @param cancelBlock The block that will be called if you don't have permission to access this data
+ */
+- (void)observeSingleEventOfType:(FIRDataEventType)eventType withBlock:(void (^)(FIRDataSnapshot *snapshot))block withCancelBlock:(nullable void (^)(NSError* error))cancelBlock;
+
+
+/**
+ * This is equivalent to observeEventType:withBlock:, except the block is immediately canceled after the initial data is returned. In addition, for FIRDataEventTypeChildAdded, FIRDataEventTypeChildMoved, and
+ * FIRDataEventTypeChildChanged events, your block will be passed the key of the previous node by priority order.
+ *
+ * The cancelBlock will be called if you do not have permission to read data at this location.
+ *
+ * @param eventType The type of event to listen for.
+ * @param block The block that should be called. It is passed the data as a FIRDataSnapshot and the previous child's key.
+ * @param cancelBlock The block that will be called if you don't have permission to access this data
+ */
+- (void)observeSingleEventOfType:(FIRDataEventType)eventType andPreviousSiblingKeyWithBlock:(void (^)(FIRDataSnapshot *snapshot, NSString *__nullable prevKey))block withCancelBlock:(nullable void (^)(NSError* error))cancelBlock;
+
+#pragma mark - Detaching observers
+
+/**
+ * Detach a block previously attached with observeEventType:withBlock:.
+ *
+ * @param handle The handle returned by the call to observeEventType:withBlock: which we are trying to remove.
+ */
+- (void) removeObserverWithHandle:(FIRDatabaseHandle)handle;
+
+/**
+ * By calling `keepSynced:YES` on a location, the data for that location will automatically be downloaded and
+ * kept in sync, even when no listeners are attached for that location. Additionally, while a location is kept
+ * synced, it will not be evicted from the persistent disk cache.
+ *
+ * @param keepSynced Pass YES to keep this location synchronized, pass NO to stop synchronization.
+ */
+- (void) keepSynced:(BOOL)keepSynced;
+
+
+/**
+ * Removes all observers at the current reference, but does not remove any observers at child references.
+ * removeAllObservers must be called again for each child reference where a listener was established to remove the observers.
+ */
+- (void) removeAllObservers;
+
+#pragma mark - Querying and limiting
+
+
+/**
+ * queryLimitedToFirst: is used to generate a reference to a limited view of the data at this location.
+ * The FIRDatabaseQuery instance returned by queryLimitedToFirst: will respond to at most the first limit child nodes.
+ *
+ * @param limit The upper bound, inclusive, for the number of child nodes to receive events for
+ * @return A FIRDatabaseQuery instance, limited to at most limit child nodes.
+ */
+- (FIRDatabaseQuery *)queryLimitedToFirst:(NSUInteger)limit;
+
+
+/**
+ * queryLimitedToLast: is used to generate a reference to a limited view of the data at this location.
+ * The FIRDatabaseQuery instance returned by queryLimitedToLast: will respond to at most the last limit child nodes.
+ *
+ * @param limit The upper bound, inclusive, for the number of child nodes to receive events for
+ * @return A FIRDatabaseQuery instance, limited to at most limit child nodes.
+ */
+- (FIRDatabaseQuery *)queryLimitedToLast:(NSUInteger)limit;
+
+/**
+ * queryOrderBy: is used to generate a reference to a view of the data that's been sorted by the values of
+ * a particular child key. This method is intended to be used in combination with queryStartingAtValue:,
+ * queryEndingAtValue:, or queryEqualToValue:.
+ *
+ * @param key The child key to use in ordering data visible to the returned FIRDatabaseQuery
+ * @return A FIRDatabaseQuery instance, ordered by the values of the specified child key.
+ */
+- (FIRDatabaseQuery *)queryOrderedByChild:(NSString *)key;
+
+/**
+ * queryOrderedByKey: is used to generate a reference to a view of the data that's been sorted by child key.
+ * This method is intended to be used in combination with queryStartingAtValue:, queryEndingAtValue:,
+ * or queryEqualToValue:.
+ *
+ * @return A FIRDatabaseQuery instance, ordered by child keys.
+ */
+- (FIRDatabaseQuery *) queryOrderedByKey;
+
+/**
+ * queryOrderedByPriority: is used to generate a reference to a view of the data that's been sorted by child
+ * priority. This method is intended to be used in combination with queryStartingAtValue:, queryEndingAtValue:,
+ * or queryEqualToValue:.
+ *
+ * @return A FIRDatabaseQuery instance, ordered by child priorities.
+ */
+- (FIRDatabaseQuery *) queryOrderedByPriority;
+
+/**
+ * queryStartingAtValue: is used to generate a reference to a limited view of the data at this location.
+ * The FIRDatabaseQuery instance returned by queryStartingAtValue: will respond to events at nodes with a value
+ * greater than or equal to startValue.
+ *
+ * @param startValue The lower bound, inclusive, for the value of data visible to the returned FIRDatabaseQuery
+ * @return A FIRDatabaseQuery instance, limited to data with value greater than or equal to startValue
+ */
+- (FIRDatabaseQuery *)queryStartingAtValue:(nullable id)startValue;
+
+/**
+ * queryStartingAtValue:childKey: is used to generate a reference to a limited view of the data at this location.
+ * The FIRDatabaseQuery instance returned by queryStartingAtValue:childKey will respond to events at nodes with a value
+ * greater than startValue, or equal to startValue and with a key greater than or equal to childKey.
+ *
+ * @param startValue The lower bound, inclusive, for the value of data visible to the returned FIRDatabaseQuery
+ * @param childKey The lower bound, inclusive, for the key of nodes with value equal to startValue
+ * @return A FIRDatabaseQuery instance, limited to data with value greater than or equal to startValue
+ */
+- (FIRDatabaseQuery *)queryStartingAtValue:(nullable id)startValue childKey:(nullable NSString *)childKey;
+
+/**
+ * queryEndingAtValue: is used to generate a reference to a limited view of the data at this location.
+ * The FIRDatabaseQuery instance returned by queryEndingAtValue: will respond to events at nodes with a value
+ * less than or equal to endValue.
+ *
+ * @param endValue The upper bound, inclusive, for the value of data visible to the returned FIRDatabaseQuery
+ * @return A FIRDatabaseQuery instance, limited to data with value less than or equal to endValue
+ */
+- (FIRDatabaseQuery *)queryEndingAtValue:(nullable id)endValue;
+
+/**
+ * queryEndingAtValue:childKey: is used to generate a reference to a limited view of the data at this location.
+ * The FIRDatabaseQuery instance returned by queryEndingAtValue:childKey will respond to events at nodes with a value
+ * less than endValue, or equal to endValue and with a key less than or equal to childKey.
+ *
+ * @param endValue The upper bound, inclusive, for the value of data visible to the returned FIRDatabaseQuery
+ * @param childKey The upper bound, inclusive, for the key of nodes with value equal to endValue
+ * @return A FIRDatabaseQuery instance, limited to data with value less than or equal to endValue
+ */
+- (FIRDatabaseQuery *)queryEndingAtValue:(nullable id)endValue childKey:(nullable NSString *)childKey;
+
+/**
+ * queryEqualToValue: is used to generate a reference to a limited view of the data at this location.
+ * The FIRDatabaseQuery instance returned by queryEqualToValue: will respond to events at nodes with a value equal
+ * to the supplied argument.
+ *
+ * @param value The value that the data returned by this FIRDatabaseQuery will have
+ * @return A FIRDatabaseQuery instance, limited to data with the supplied value.
+ */
+- (FIRDatabaseQuery *)queryEqualToValue:(nullable id)value;
+
+/**
+ * queryEqualToValue:childKey: is used to generate a reference to a limited view of the data at this location.
+ * The FIRDatabaseQuery instance returned by queryEqualToValue:childKey will respond to events at nodes with a value
+ * equal to the supplied argument with a key equal to childKey. There will be at most one node that matches because
+ * child keys are unique.
+ *
+ * @param value The value that the data returned by this FIRDatabaseQuery will have
+ * @param childKey The key of nodes with the right value
+ * @return A FIRDatabaseQuery instance, limited to data with the supplied value and the key.
+ */
+- (FIRDatabaseQuery *)queryEqualToValue:(nullable id)value childKey:(nullable NSString *)childKey;
+
+#pragma mark - Managing presence
+
+/**
+ * Ensure the data at this location is set to the specified value when
+ * the client is disconnected (due to closing the browser, navigating
+ * to a new page, or network issues).
+ *
+ * onDisconnectSetValue: is especially useful for implementing "presence" systems,
+ * where a value should be changed or cleared when a user disconnects
+ * so that he appears "offline" to other users.
+ *
+ * @param value The value to be set after the connection is lost.
+ */
+- (void) onDisconnectSetValue:(nullable id)value;
+
+
+/**
+ * Ensure the data at this location is set to the specified value when
+ * the client is disconnected (due to closing the browser, navigating
+ * to a new page, or network issues).
+ *
+ * The completion block will be triggered when the operation has been successfully queued up on the Firebase Database servers
+ *
+ * @param value The value to be set after the connection is lost.
+ * @param block Block to be triggered when the operation has been queued up on the Firebase Database servers
+ */
+- (void) onDisconnectSetValue:(nullable id)value withCompletionBlock:(void (^)(NSError *__nullable error, FIRDatabaseReference * ref))block;
+
+
+/**
+ * Ensure the data at this location is set to the specified value and priority when
+ * the client is disconnected (due to closing the browser, navigating
+ * to a new page, or network issues).
+ *
+ * @param value The value to be set after the connection is lost.
+ * @param priority The priority to be set after the connection is lost.
+ */
+- (void) onDisconnectSetValue:(nullable id)value andPriority:(id)priority;
+
+
+/**
+ * Ensure the data at this location is set to the specified value and priority when
+ * the client is disconnected (due to closing the browser, navigating
+ * to a new page, or network issues).
+ *
+ * The completion block will be triggered when the operation has been successfully queued up on the Firebase Database servers
+ *
+ * @param value The value to be set after the connection is lost.
+ * @param priority The priority to be set after the connection is lost.
+ * @param block Block to be triggered when the operation has been queued up on the Firebase Database servers
+ */
+- (void) onDisconnectSetValue:(nullable id)value andPriority:(nullable id)priority withCompletionBlock:(void (^)(NSError *__nullable error, FIRDatabaseReference * ref))block;
+
+
+/**
+ * Ensure the data at this location is removed when
+ * the client is disconnected (due to closing the app, navigating
+ * to a new page, or network issues).
+ *
+ * onDisconnectRemoveValue is especially useful for implementing "presence" systems.
+ */
+- (void) onDisconnectRemoveValue;
+
+
+/**
+ * Ensure the data at this location is removed when
+ * the client is disconnected (due to closing the app, navigating
+ * to a new page, or network issues).
+ *
+ * onDisconnectRemoveValueWithCompletionBlock: is especially useful for implementing "presence" systems.
+ *
+ * @param block Block to be triggered when the operation has been queued up on the Firebase Database servers
+ */
+- (void) onDisconnectRemoveValueWithCompletionBlock:(void (^)(NSError *__nullable error, FIRDatabaseReference * ref))block;
+
+
+
+/**
+ * Ensure the data has the specified child values updated when
+ * the client is disconnected (due to closing the browser, navigating
+ * to a new page, or network issues).
+ *
+ *
+ * @param values A dictionary of child node keys and the values to set them to after the connection is lost.
+ */
+- (void) onDisconnectUpdateChildValues:(NSDictionary *)values;
+
+
+/**
+ * Ensure the data has the specified child values updated when
+ * the client is disconnected (due to closing the browser, navigating
+ * to a new page, or network issues).
+ *
+ *
+ * @param values A dictionary of child node keys and the values to set them to after the connection is lost.
+ * @param block A block that will be called once the operation has been queued up on the Firebase Database servers
+ */
+- (void) onDisconnectUpdateChildValues:(NSDictionary *)values withCompletionBlock:(void (^)(NSError *__nullable error, FIRDatabaseReference * ref))block;
+
+
+/**
+ * Cancel any operations that are set to run on disconnect. If you previously called onDisconnectSetValue:,
+ * onDisconnectRemoveValue:, or onDisconnectUpdateChildValues:, and no longer want the values updated when the
+ * connection is lost, call cancelDisconnectOperations:
+ */
+- (void) cancelDisconnectOperations;
+
+
+/**
+ * Cancel any operations that are set to run on disconnect. If you previously called onDisconnectSetValue:,
+ * onDisconnectRemoveValue:, or onDisconnectUpdateChildValues:, and no longer want the values updated when the
+ * connection is lost, call cancelDisconnectOperations:
+ *
+ * @param block A block that will be triggered once the Firebase Database servers have acknowledged the cancel request.
+ */
+- (void) cancelDisconnectOperationsWithCompletionBlock:(nullable void (^)(NSError *__nullable error, FIRDatabaseReference * ref))block;
+
+
+#pragma mark - Manual Connection Management
+
+/**
+ * Manually disconnect the Firebase Database client from the server and disable automatic reconnection.
+ *
+ * The Firebase Database client automatically maintains a persistent connection to the Firebase Database server,
+ * which will remain active indefinitely and reconnect when disconnected. However, the goOffline( )
+ * and goOnline( ) methods may be used to manually control the client connection in cases where
+ * a persistent connection is undesirable.
+ *
+ * While offline, the Firebase Database client will no longer receive data updates from the server. However,
+ * all database operations performed locally will continue to immediately fire events, allowing
+ * your application to continue behaving normally. Additionally, each operation performed locally
+ * will automatically be queued and retried upon reconnection to the Firebase Database server.
+ *
+ * To reconnect to the Firebase Database server and begin receiving remote events, see goOnline( ).
+ * Once the connection is reestablished, the Firebase Database client will transmit the appropriate data
+ * and fire the appropriate events so that your client "catches up" automatically.
+ *
+ * Note: Invoking this method will impact all Firebase Database connections.
+ */
++ (void) goOffline;
+
+/**
+ * Manually reestablish a connection to the Firebase Database server and enable automatic reconnection.
+ *
+ * The Firebase Database client automatically maintains a persistent connection to the Firebase Database server,
+ * which will remain active indefinitely and reconnect when disconnected. However, the goOffline( )
+ * and goOnline( ) methods may be used to manually control the client connection in cases where
+ * a persistent connection is undesirable.
+ *
+ * This method should be used after invoking goOffline( ) to disable the active connection.
+ * Once reconnected, the Firebase Database client will automatically transmit the proper data and fire
+ * the appropriate events so that your client "catches up" automatically.
+ *
+ * To disconnect from the Firebase Database server, see goOffline( ).
+ *
+ * Note: Invoking this method will impact all Firebase Database connections.
+ */
++ (void) goOnline;
+
+
+#pragma mark - Transactions
+
+/**
+ * Performs an optimistic-concurrency transactional update to the data at this location. Your block will be called with a FIRMutableData
+ * instance that contains the current data at this location. Your block should update this data to the value you
+ * wish to write to this location, and then return an instance of FIRTransactionResult with the new data.
+ *
+ * If, when the operation reaches the server, it turns out that this client had stale data, your block will be run
+ * again with the latest data from the server.
+ *
+ * When your block is run, you may decide to abort the transaction by returning [FIRTransactionResult abort].
+ *
+ * @param block This block receives the current data at this location and must return an instance of FIRTransactionResult
+ */
+- (void) runTransactionBlock:(FIRTransactionResult * (^) (FIRMutableData* currentData))block;
+
+
+/**
+ * Performs an optimistic-concurrency transactional update to the data at this location. Your block will be called with a FIRMutableData
+ * instance that contains the current data at this location. Your block should update this data to the value you
+ * wish to write to this location, and then return an instance of FIRTransactionResult with the new data.
+ *
+ * If, when the operation reaches the server, it turns out that this client had stale data, your block will be run
+ * again with the latest data from the server.
+ *
+ * When your block is run, you may decide to abort the transaction by returning [FIRTransactionResult abort].
+ *
+ * @param block This block receives the current data at this location and must return an instance of FIRTransactionResult
+ * @param completionBlock This block will be triggered once the transaction is complete, whether it was successful or not. It will indicate if there was an error, whether or not the data was committed, and what the current value of the data at this location is.
+ */
+- (void)runTransactionBlock:(FIRTransactionResult * (^) (FIRMutableData* currentData))block andCompletionBlock:(void (^) (NSError *__nullable error, BOOL committed, FIRDataSnapshot *__nullable snapshot))completionBlock;
+
+
+
+/**
+ * Performs an optimistic-concurrency transactional update to the data at this location. Your block will be called with a FIRMutableData
+ * instance that contains the current data at this location. Your block should update this data to the value you
+ * wish to write to this location, and then return an instance of FIRTransactionResult with the new data.
+ *
+ * If, when the operation reaches the server, it turns out that this client had stale data, your block will be run
+ * again with the latest data from the server.
+ *
+ * When your block is run, you may decide to abort the transaction by return [FIRTransactionResult abort].
+ *
+ * Since your block may be run multiple times, this client could see several immediate states that don't exist on the server. You can suppress those immediate states until the server confirms the final state of the transaction.
+ *
+ * @param block This block receives the current data at this location and must return an instance of FIRTransactionResult
+ * @param completionBlock This block will be triggered once the transaction is complete, whether it was successful or not. It will indicate if there was an error, whether or not the data was committed, and what the current value of the data at this location is.
+ * @param localEvents Set this to NO to suppress events raised for intermediate states, and only get events based on the final state of the transaction.
+ */
+- (void)runTransactionBlock:(FIRTransactionResult * (^) (FIRMutableData* currentData))block andCompletionBlock:(nullable void (^) (NSError *__nullable error, BOOL committed, FIRDataSnapshot *__nullable snapshot))completionBlock withLocalEvents:(BOOL)localEvents;
+
+
+#pragma mark - Retrieving String Representation
+
+/**
+ * Gets the absolute URL of this Firebase Database location.
+ *
+ * @return The absolute URL of the referenced Firebase Database location.
+ */
+- (NSString *) description;
+
+#pragma mark - Properties
+
+/**
+ * Gets a FIRDatabaseReference for the parent location.
+ * If this instance refers to the root of your Firebase Database, it has no parent,
+ * and therefore parent( ) will return null.
+ *
+ * @return A FIRDatabaseReference for the parent location.
+ */
+@property (strong, readonly, nonatomic, nullable) FIRDatabaseReference * parent;
+
+
+/**
+ * Gets a FIRDatabaseReference for the root location
+ *
+ * @return A new FIRDatabaseReference to root location.
+ */
+@property (strong, readonly, nonatomic) FIRDatabaseReference * root;
+
+
+/**
+ * Gets the last token in a Firebase Database location (e.g. 'fred' in https&#58;//SampleChat.firebaseIO-demo.com/users/fred)
+ *
+ * @return The key of the location this reference points to.
+ */
+@property (strong, readonly, nonatomic) NSString* key;
+
+/**
+ * Gets the URL for the Firebase Database location referenced by this FIRDatabaseReference.
+ *
+ * @return The url of the location this reference points to.
+ */
+@property (strong, readonly, nonatomic) NSString* URL;
+
+/**
+ * Gets the FIRDatabase instance associated with this reference.
+ *
+ * @return The FIRDatabase object for this reference.
+ */
+@property (strong, readonly, nonatomic) FIRDatabase *database;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Database/FIRDatabaseReference.m b/Firebase/Database/FIRDatabaseReference.m
new file mode 100644
index 0000000..4f27493
--- /dev/null
+++ b/Firebase/Database/FIRDatabaseReference.m
@@ -0,0 +1,404 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRApp.h"
+#import "FIRDatabaseReference.h"
+#import "FIROptions.h"
+#import "FUtilities.h"
+#import "FNextPushId.h"
+#import "FIRDatabaseQuery_Private.h"
+#import "FValidation.h"
+#import "FIRDatabaseReference_Private.h"
+#import "FStringUtilities.h"
+#import "FSnapshotUtilities.h"
+#import "FIRDatabaseConfig.h"
+#import "FIRDatabaseConfig_Private.h"
+#import "FQueryParams.h"
+#import "FIRDatabase.h"
+
+@implementation FIRDatabaseReference
+
++ (FIRDatabaseConfig *)defaultConfig {
+ return [FIRDatabaseConfig defaultConfig];
+}
+
+#pragma mark -
+#pragma mark Constructors
+
+- (id) initWithConfig:(FIRDatabaseConfig *)config {
+ FParsedUrl* parsedUrl = [FUtilities parseUrl:[[FIRApp defaultApp] options].databaseURL];
+ [FValidation validateFrom:@"initWithUrl:" validURL:parsedUrl];
+ return [self initWithRepo:[FRepoManager getRepo:parsedUrl.repoInfo config:config] path:parsedUrl.path];
+}
+
+- (id) initWithRepo:(FRepo *)repo path:(FPath *)path {
+ return [super initWithRepo:repo
+ path:path
+ params:[FQueryParams defaultInstance]
+ orderByCalled:NO
+ priorityMethodCalled:NO];
+}
+
+
+#pragma mark -
+#pragma mark Ancillary methods
+
+- (NSString *) key {
+ if([self.path isEmpty]) {
+ return nil;
+ }
+ else {
+ return [self.path getBack];
+ }
+}
+
+- (FIRDatabase *) database {
+ return self.repo.database;
+}
+
+- (FIRDatabaseReference *) parent {
+ FPath* parentPath = [self.path parent];
+ FIRDatabaseReference * parent = nil;
+ if (parentPath != nil ) {
+ parent = [[FIRDatabaseReference alloc] initWithRepo:self.repo path:parentPath];
+ }
+ return parent;
+}
+
+- (NSString *) URL {
+ FIRDatabaseReference * parent = [self parent];
+ return parent == nil ? [self.repo description] : [NSString stringWithFormat:@"%@/%@", [parent description], [FStringUtilities urlEncoded:self.key]];
+}
+
+- (NSString *) description {
+ return [self URL];
+}
+
+- (FIRDatabaseReference *) root {
+ return [[FIRDatabaseReference alloc] initWithRepo:self.repo path:[[FPath alloc] initWith:@""]];
+}
+
+#pragma mark -
+#pragma mark Child methods
+
+- (FIRDatabaseReference *)childByAppendingPath:(NSString *)pathString {
+ return [self child:pathString];
+}
+
+- (FIRDatabaseReference *)child:(NSString *)pathString {
+ if ([self.path getFront] == nil) {
+ // we're at the root
+ [FValidation validateFrom:@"child:" validRootPathString:pathString];
+ } else {
+ [FValidation validateFrom:@"child:" validPathString:pathString];
+ }
+ FPath* path = [self.path childFromString:pathString];
+ FIRDatabaseReference * firebaseRef = [[FIRDatabaseReference alloc] initWithRepo:self.repo path:path];
+ return firebaseRef;
+}
+
+- (FIRDatabaseReference *) childByAutoId {
+ [FValidation validateFrom:@"childByAutoId:" writablePath:self.path];
+
+ NSString* name = [FNextPushId get:self.repo.serverTime];
+ return [self child:name];
+}
+
+#pragma mark -
+#pragma mark Basic write methods
+
+- (void) setValue:(id)value {
+ [self setValueInternal:value andPriority:nil withCompletionBlock:nil from:@"setValue:"];
+}
+
+- (void) setValue:(id)value withCompletionBlock:(fbt_void_nserror_ref)block {
+ [self setValueInternal:value andPriority:nil withCompletionBlock:block from:@"setValue:withCompletionBlock:"];
+}
+
+- (void) setValue:(id)value andPriority:(id)priority {
+ [self setValueInternal:value andPriority:priority withCompletionBlock:nil from:@"setValue:andPriority:"];
+}
+
+- (void) setValue:(id)value andPriority:(id)priority withCompletionBlock:(fbt_void_nserror_ref)block {
+ [self setValueInternal:value andPriority:priority withCompletionBlock:block from:@"setValue:andPriority:withCompletionBlock:"];
+}
+
+- (void) setValueInternal:(id)value andPriority:(id)priority withCompletionBlock:(fbt_void_nserror_ref)block from:(NSString*)fn {
+ [FValidation validateFrom:fn writablePath:self.path];
+
+ fbt_void_nserror_ref userCallback = [block copy];
+ id<FNode> newNode = [FSnapshotUtilities nodeFrom:value priority:priority withValidationFrom:fn];
+
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ [self.repo set:self.path withNode:newNode withCallback:userCallback];
+ });
+}
+
+
+- (void) removeValue {
+ [self setValueInternal:nil andPriority:nil withCompletionBlock:nil from:@"removeValue:"];
+}
+
+- (void) removeValueWithCompletionBlock:(fbt_void_nserror_ref)block {
+ [self setValueInternal:nil andPriority:nil withCompletionBlock:block from:@"removeValueWithCompletionBlock:"];
+}
+
+
+- (void) setPriority:(id)priority {
+ [self setPriorityInternal:priority withCompletionBlock:nil from:@"setPriority:"];
+}
+
+- (void) setPriority:(id)priority withCompletionBlock:(fbt_void_nserror_ref)block {
+
+ [self setPriorityInternal:priority withCompletionBlock:block from:@"setPriority:withCompletionBlock:"];
+}
+
+- (void) setPriorityInternal:(id)priority withCompletionBlock:(fbt_void_nserror_ref)block from:(NSString*)fn {
+ [FValidation validateFrom:fn writablePath:self.path];
+
+ fbt_void_nserror_ref userCallback = [block copy];
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ [self.repo set:[self.path childFromString:@".priority"] withNode:[FSnapshotUtilities nodeFrom:priority] withCallback:userCallback];
+ });
+}
+
+
+- (void) updateChildValues:(NSDictionary *)values {
+ [self updateChildValuesInternal:values withCompletionBlock:nil from:@"updateChildValues:"];
+}
+
+- (void) updateChildValues:(NSDictionary *)values withCompletionBlock:(fbt_void_nserror_ref)block {
+ [self updateChildValuesInternal:values withCompletionBlock:block from:@"updateChildValues:withCompletionBlock:"];
+}
+
+- (void) updateChildValuesInternal:(NSDictionary *)values withCompletionBlock:(fbt_void_nserror_ref)block from:(NSString*)fn {
+ [FValidation validateFrom:fn writablePath:self.path];
+
+ FCompoundWrite *merge = [FSnapshotUtilities compoundWriteFromDictionary:values withValidationFrom:fn];
+
+ fbt_void_nserror_ref userCallback = [block copy];
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ [self.repo update:self.path withNodes:merge withCallback:userCallback];
+ });
+}
+
+#pragma mark -
+#pragma mark Disconnect Operations
+
+- (void) onDisconnectSetValue:(id)value {
+ [self onDisconnectSetValueInternal:value andPriority:nil withCompletionBlock:nil from:@"onDisconnectSetValue:"];
+}
+
+- (void) onDisconnectSetValue:(id)value withCompletionBlock:(fbt_void_nserror_ref)block {
+ [self onDisconnectSetValueInternal:value andPriority:nil withCompletionBlock:block from:@"onDisconnectSetValue:withCompletionBlock:"];
+}
+
+- (void) onDisconnectSetValue:(id)value andPriority:(id)priority {
+ [self onDisconnectSetValueInternal:value andPriority:priority withCompletionBlock:nil from:@"onDisconnectSetValue:andPriority:"];
+}
+
+- (void) onDisconnectSetValue:(id)value andPriority:(id)priority withCompletionBlock:(fbt_void_nserror_ref)block {
+ [self onDisconnectSetValueInternal:value andPriority:priority withCompletionBlock:block from:@"onDisconnectSetValue:andPriority:withCompletionBlock:"];
+}
+
+- (void) onDisconnectSetValueInternal:(id)value andPriority:(id)priority withCompletionBlock:(fbt_void_nserror_ref)block from:(NSString*)fn {
+ [FValidation validateFrom:fn writablePath:self.path];
+
+ id<FNode> newNodeUnresolved = [FSnapshotUtilities nodeFrom:value priority:priority withValidationFrom:fn];
+
+ fbt_void_nserror_ref userCallback = [block copy];
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ [self.repo onDisconnectSet:self.path withNode:newNodeUnresolved withCallback:userCallback];
+ });
+}
+
+
+- (void) onDisconnectRemoveValue {
+ [self onDisconnectSetValueInternal:nil andPriority:nil withCompletionBlock:nil from:@"onDisconnectRemoveValue:"];
+}
+
+- (void) onDisconnectRemoveValueWithCompletionBlock:(fbt_void_nserror_ref)block {
+ [self onDisconnectSetValueInternal:nil andPriority:nil withCompletionBlock:block from:@"onDisconnectRemoveValueWithCompletionBlock:"];
+}
+
+
+- (void) onDisconnectUpdateChildValues:(NSDictionary *)values {
+ [self onDisconnectUpdateChildValuesInternal:values withCompletionBlock:nil from:@"onDisconnectUpdateChildValues:"];
+}
+
+- (void) onDisconnectUpdateChildValues:(NSDictionary *)values withCompletionBlock:(fbt_void_nserror_ref)block {
+ [self onDisconnectUpdateChildValuesInternal:values withCompletionBlock:block from:@"onDisconnectUpdateChildValues:withCompletionBlock:"];
+}
+
+- (void) onDisconnectUpdateChildValuesInternal:(NSDictionary *)values withCompletionBlock:(fbt_void_nserror_ref)block from:(NSString*)fn {
+ [FValidation validateFrom:fn writablePath:self.path];
+
+ FCompoundWrite *merge = [FSnapshotUtilities compoundWriteFromDictionary:values withValidationFrom:fn];
+
+ fbt_void_nserror_ref userCallback = [block copy];
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ [self.repo onDisconnectUpdate:self.path withNodes:merge withCallback:userCallback];
+ });
+}
+
+
+- (void) cancelDisconnectOperations {
+ [self cancelDisconnectOperationsWithCompletionBlock:nil];
+}
+
+- (void) cancelDisconnectOperationsWithCompletionBlock:(fbt_void_nserror_ref)block {
+ fbt_void_nserror_ref callback = nil;
+ if (block != nil) {
+ callback = [block copy];
+ }
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ [self.repo onDisconnectCancel:self.path withCallback:callback];
+ });
+}
+
+#pragma mark -
+#pragma mark Connection management methods
+
++ (void) goOffline {
+ [FRepoManager interruptAll];
+}
+
++ (void) goOnline {
+ [FRepoManager resumeAll];
+}
+
+
+#pragma mark -
+#pragma mark Data reading methods deferred to FQuery
+
+- (FIRDatabaseHandle)observeEventType:(FIRDataEventType)eventType withBlock:(fbt_void_datasnapshot)block {
+ return [self observeEventType:eventType withBlock:block withCancelBlock:nil];
+}
+
+- (FIRDatabaseHandle)observeEventType:(FIRDataEventType)eventType andPreviousSiblingKeyWithBlock:(fbt_void_datasnapshot_nsstring)block {
+ return [self observeEventType:eventType andPreviousSiblingKeyWithBlock:block withCancelBlock:nil];
+}
+
+- (FIRDatabaseHandle)observeEventType:(FIRDataEventType)eventType withBlock:(fbt_void_datasnapshot)block withCancelBlock:(fbt_void_nserror)cancelBlock {
+ return [super observeEventType:eventType withBlock:block withCancelBlock:cancelBlock];
+}
+
+- (FIRDatabaseHandle)observeEventType:(FIRDataEventType)eventType andPreviousSiblingKeyWithBlock:(fbt_void_datasnapshot_nsstring)block withCancelBlock:(fbt_void_nserror)cancelBlock {
+ return [super observeEventType:eventType andPreviousSiblingKeyWithBlock:block withCancelBlock:cancelBlock];
+}
+
+
+- (void) removeObserverWithHandle:(FIRDatabaseHandle)handle {
+ [super removeObserverWithHandle:handle];
+}
+
+
+- (void) removeAllObservers {
+ [super removeAllObservers];
+}
+
+- (void) keepSynced:(BOOL)keepSynced {
+ [super keepSynced:keepSynced];
+}
+
+- (void)observeSingleEventOfType:(FIRDataEventType)eventType withBlock:(fbt_void_datasnapshot)block {
+ [self observeSingleEventOfType:eventType withBlock:block withCancelBlock:nil];
+}
+
+- (void)observeSingleEventOfType:(FIRDataEventType)eventType andPreviousSiblingKeyWithBlock:(fbt_void_datasnapshot_nsstring)block {
+ [self observeSingleEventOfType:eventType andPreviousSiblingKeyWithBlock:block withCancelBlock:nil];
+}
+
+- (void)observeSingleEventOfType:(FIRDataEventType)eventType withBlock:(fbt_void_datasnapshot)block withCancelBlock:(fbt_void_nserror)cancelBlock {
+ [super observeSingleEventOfType:eventType withBlock:block withCancelBlock:cancelBlock];
+}
+
+- (void)observeSingleEventOfType:(FIRDataEventType)eventType andPreviousSiblingKeyWithBlock:(fbt_void_datasnapshot_nsstring)block withCancelBlock:(fbt_void_nserror)cancelBlock {
+ [super observeSingleEventOfType:eventType andPreviousSiblingKeyWithBlock:block withCancelBlock:cancelBlock];
+}
+
+#pragma mark -
+#pragma mark Query methods
+// These methods suppress warnings from having method definitions in FIRDatabaseReference.h for docs generation.
+
+- (FIRDatabaseQuery *)queryLimitedToFirst:(NSUInteger)limit {
+ return [super queryLimitedToFirst:limit];
+}
+
+- (FIRDatabaseQuery *)queryLimitedToLast:(NSUInteger)limit {
+ return [super queryLimitedToLast:limit];
+}
+
+- (FIRDatabaseQuery *)queryOrderedByChild:(NSString *)key {
+ return [super queryOrderedByChild:key];
+}
+
+- (FIRDatabaseQuery *) queryOrderedByKey {
+ return [super queryOrderedByKey];
+}
+
+- (FIRDatabaseQuery *) queryOrderedByPriority {
+ return [super queryOrderedByPriority];
+}
+
+- (FIRDatabaseQuery *)queryStartingAtValue:(id)startValue {
+ return [super queryStartingAtValue:startValue];
+}
+
+- (FIRDatabaseQuery *)queryStartingAtValue:(id)startValue childKey:(NSString *)childKey {
+ return [super queryStartingAtValue:startValue childKey:childKey];
+}
+
+- (FIRDatabaseQuery *)queryEndingAtValue:(id)endValue {
+ return [super queryEndingAtValue:endValue];
+}
+
+- (FIRDatabaseQuery *)queryEndingAtValue:(id)endValue childKey:(NSString *)childKey {
+ return [super queryEndingAtValue:endValue childKey:childKey];
+}
+
+- (FIRDatabaseQuery *)queryEqualToValue:(id)value {
+ return [super queryEqualToValue:value];
+}
+
+- (FIRDatabaseQuery *)queryEqualToValue:(id)value childKey:(NSString *)childKey {
+ return [super queryEqualToValue:value childKey:childKey];
+}
+
+
+#pragma mark -
+#pragma mark Transaction methods
+
+- (void) runTransactionBlock:(fbt_transactionresult_mutabledata)block {
+ [FValidation validateFrom:@"runTransactionBlock:" writablePath:self.path];
+ [self runTransactionBlock:block andCompletionBlock:nil withLocalEvents:YES];
+}
+
+- (void) runTransactionBlock:(fbt_transactionresult_mutabledata)update andCompletionBlock:(fbt_void_nserror_bool_datasnapshot)completionBlock {
+ [FValidation validateFrom:@"runTransactionBlock:andCompletionBlock:" writablePath:self.path];
+ [self runTransactionBlock:update andCompletionBlock:completionBlock withLocalEvents:YES];
+}
+
+- (void) runTransactionBlock:(fbt_transactionresult_mutabledata)block andCompletionBlock:(fbt_void_nserror_bool_datasnapshot)completionBlock withLocalEvents:(BOOL)localEvents {
+ [FValidation validateFrom:@"runTransactionBlock:andCompletionBlock:withLocalEvents:" writablePath:self.path];
+ fbt_transactionresult_mutabledata updateCopy = [block copy];
+ fbt_void_nserror_bool_datasnapshot onCompleteCopy = [completionBlock copy];
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ [self.repo startTransactionOnPath:self.path update:updateCopy onComplete:onCompleteCopy withLocalEvents:localEvents];
+ });
+}
+
+@end
diff --git a/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary.xcodeproj/project.pbxproj b/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary.xcodeproj/project.pbxproj
new file mode 100644
index 0000000..ef72cf0
--- /dev/null
+++ b/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary.xcodeproj/project.pbxproj
@@ -0,0 +1,438 @@
+// !$*UTF8*$!
+{
+ archiveVersion = 1;
+ classes = {
+ };
+ objectVersion = 46;
+ objects = {
+
+/* Begin PBXBuildFile section */
+ EDB1C0A11653283D0041897E /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EDB1C0A01653283D0041897E /* Foundation.framework */; };
+ EDB1C0B01653283D0041897E /* SenTestingKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EDB1C0AF1653283D0041897E /* SenTestingKit.framework */; };
+ EDB1C0B21653283D0041897E /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EDB1C0B11653283D0041897E /* UIKit.framework */; };
+ EDB1C0B31653283D0041897E /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EDB1C0A01653283D0041897E /* Foundation.framework */; };
+ EDB1C0B61653283D0041897E /* libFImmutableSortedDictionary.a in Frameworks */ = {isa = PBXBuildFile; fileRef = EDB1C09D1653283D0041897E /* libFImmutableSortedDictionary.a */; };
+ EDB1C0BC1653283D0041897E /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = EDB1C0BA1653283D0041897E /* InfoPlist.strings */; };
+ EDB1C0BF1653283D0041897E /* FImmutableSortedDictionaryTests.m in Sources */ = {isa = PBXBuildFile; fileRef = EDB1C0BE1653283D0041897E /* FImmutableSortedDictionaryTests.m */; };
+ EDB1C0D21653286B0041897E /* FImmutableSortedDictionary.m in Sources */ = {isa = PBXBuildFile; fileRef = EDB1C0CC1653286B0041897E /* FImmutableSortedDictionary.m */; };
+ EDB1C0D31653286B0041897E /* FImmutableSortedDictionary.m in Sources */ = {isa = PBXBuildFile; fileRef = EDB1C0CC1653286B0041897E /* FImmutableSortedDictionary.m */; };
+ EDB1C0D41653286B0041897E /* FLLRBEmptyNode.m in Sources */ = {isa = PBXBuildFile; fileRef = EDB1C0CF1653286B0041897E /* FLLRBEmptyNode.m */; };
+ EDB1C0D51653286B0041897E /* FLLRBEmptyNode.m in Sources */ = {isa = PBXBuildFile; fileRef = EDB1C0CF1653286B0041897E /* FLLRBEmptyNode.m */; };
+ EDB1C0D61653286B0041897E /* FLLRBValueNode.m in Sources */ = {isa = PBXBuildFile; fileRef = EDB1C0D11653286B0041897E /* FLLRBValueNode.m */; };
+ EDB1C0D71653286B0041897E /* FLLRBValueNode.m in Sources */ = {isa = PBXBuildFile; fileRef = EDB1C0D11653286B0041897E /* FLLRBValueNode.m */; };
+ EDB1C0ED165331140041897E /* FImmutableSortedDictionaryEnumerator.m in Sources */ = {isa = PBXBuildFile; fileRef = EDB1C0EC165331140041897E /* FImmutableSortedDictionaryEnumerator.m */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXContainerItemProxy section */
+ EDB1C0B41653283D0041897E /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = EDB1C0941653283D0041897E /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = EDB1C09C1653283D0041897E;
+ remoteInfo = FImmutableSortedDictionary;
+ };
+/* End PBXContainerItemProxy section */
+
+/* Begin PBXCopyFilesBuildPhase section */
+ EDB1C09B1653283D0041897E /* CopyFiles */ = {
+ isa = PBXCopyFilesBuildPhase;
+ buildActionMask = 2147483647;
+ dstPath = "include/${PRODUCT_NAME}";
+ dstSubfolderSpec = 16;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXCopyFilesBuildPhase section */
+
+/* Begin PBXFileReference section */
+ EDB1C09D1653283D0041897E /* libFImmutableSortedDictionary.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libFImmutableSortedDictionary.a; sourceTree = BUILT_PRODUCTS_DIR; };
+ EDB1C0A01653283D0041897E /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; };
+ EDB1C0A41653283D0041897E /* FImmutableSortedDictionary-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "FImmutableSortedDictionary-Prefix.pch"; sourceTree = "<group>"; };
+ EDB1C0AE1653283D0041897E /* FImmutableSortedDictionaryTests.octest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = FImmutableSortedDictionaryTests.octest; sourceTree = BUILT_PRODUCTS_DIR; };
+ EDB1C0AF1653283D0041897E /* SenTestingKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SenTestingKit.framework; path = Library/Frameworks/SenTestingKit.framework; sourceTree = DEVELOPER_DIR; };
+ EDB1C0B11653283D0041897E /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = Library/Frameworks/UIKit.framework; sourceTree = DEVELOPER_DIR; };
+ EDB1C0B91653283D0041897E /* FImmutableSortedDictionaryTests-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "FImmutableSortedDictionaryTests-Info.plist"; sourceTree = "<group>"; };
+ EDB1C0BB1653283D0041897E /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = "<group>"; };
+ EDB1C0BD1653283D0041897E /* FImmutableSortedDictionaryTests.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FImmutableSortedDictionaryTests.h; sourceTree = "<group>"; };
+ EDB1C0BE1653283D0041897E /* FImmutableSortedDictionaryTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FImmutableSortedDictionaryTests.m; sourceTree = "<group>"; };
+ EDB1C0CB1653286B0041897E /* FImmutableSortedDictionary.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FImmutableSortedDictionary.h; sourceTree = "<group>"; };
+ EDB1C0CC1653286B0041897E /* FImmutableSortedDictionary.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FImmutableSortedDictionary.m; sourceTree = "<group>"; };
+ EDB1C0CD1653286B0041897E /* FLLRBNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FLLRBNode.h; sourceTree = "<group>"; };
+ EDB1C0CE1653286B0041897E /* FLLRBEmptyNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FLLRBEmptyNode.h; sourceTree = "<group>"; };
+ EDB1C0CF1653286B0041897E /* FLLRBEmptyNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLLRBEmptyNode.m; sourceTree = "<group>"; };
+ EDB1C0D01653286B0041897E /* FLLRBValueNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FLLRBValueNode.h; sourceTree = "<group>"; };
+ EDB1C0D11653286B0041897E /* FLLRBValueNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLLRBValueNode.m; sourceTree = "<group>"; };
+ EDB1C0EB165331140041897E /* FImmutableSortedDictionaryEnumerator.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FImmutableSortedDictionaryEnumerator.h; sourceTree = "<group>"; };
+ EDB1C0EC165331140041897E /* FImmutableSortedDictionaryEnumerator.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FImmutableSortedDictionaryEnumerator.m; sourceTree = "<group>"; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+ EDB1C09A1653283D0041897E /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ EDB1C0A11653283D0041897E /* Foundation.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ EDB1C0AA1653283D0041897E /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ EDB1C0B01653283D0041897E /* SenTestingKit.framework in Frameworks */,
+ EDB1C0B21653283D0041897E /* UIKit.framework in Frameworks */,
+ EDB1C0B31653283D0041897E /* Foundation.framework in Frameworks */,
+ EDB1C0B61653283D0041897E /* libFImmutableSortedDictionary.a in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+ EDB1C0921653283D0041897E = {
+ isa = PBXGroup;
+ children = (
+ EDB1C0A21653283D0041897E /* FImmutableSortedDictionary */,
+ EDB1C0B71653283D0041897E /* FImmutableSortedDictionaryTests */,
+ EDB1C09F1653283D0041897E /* Frameworks */,
+ EDB1C09E1653283D0041897E /* Products */,
+ );
+ sourceTree = "<group>";
+ };
+ EDB1C09E1653283D0041897E /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ EDB1C09D1653283D0041897E /* libFImmutableSortedDictionary.a */,
+ EDB1C0AE1653283D0041897E /* FImmutableSortedDictionaryTests.octest */,
+ );
+ name = Products;
+ sourceTree = "<group>";
+ };
+ EDB1C09F1653283D0041897E /* Frameworks */ = {
+ isa = PBXGroup;
+ children = (
+ EDB1C0A01653283D0041897E /* Foundation.framework */,
+ EDB1C0AF1653283D0041897E /* SenTestingKit.framework */,
+ EDB1C0B11653283D0041897E /* UIKit.framework */,
+ );
+ name = Frameworks;
+ sourceTree = "<group>";
+ };
+ EDB1C0A21653283D0041897E /* FImmutableSortedDictionary */ = {
+ isa = PBXGroup;
+ children = (
+ EDB1C0CB1653286B0041897E /* FImmutableSortedDictionary.h */,
+ EDB1C0CC1653286B0041897E /* FImmutableSortedDictionary.m */,
+ EDB1C0CD1653286B0041897E /* FLLRBNode.h */,
+ EDB1C0CE1653286B0041897E /* FLLRBEmptyNode.h */,
+ EDB1C0CF1653286B0041897E /* FLLRBEmptyNode.m */,
+ EDB1C0D01653286B0041897E /* FLLRBValueNode.h */,
+ EDB1C0D11653286B0041897E /* FLLRBValueNode.m */,
+ EDB1C0EB165331140041897E /* FImmutableSortedDictionaryEnumerator.h */,
+ EDB1C0EC165331140041897E /* FImmutableSortedDictionaryEnumerator.m */,
+ EDB1C0A31653283D0041897E /* Supporting Files */,
+ );
+ path = FImmutableSortedDictionary;
+ sourceTree = "<group>";
+ };
+ EDB1C0A31653283D0041897E /* Supporting Files */ = {
+ isa = PBXGroup;
+ children = (
+ EDB1C0A41653283D0041897E /* FImmutableSortedDictionary-Prefix.pch */,
+ );
+ name = "Supporting Files";
+ sourceTree = "<group>";
+ };
+ EDB1C0B71653283D0041897E /* FImmutableSortedDictionaryTests */ = {
+ isa = PBXGroup;
+ children = (
+ EDB1C0BD1653283D0041897E /* FImmutableSortedDictionaryTests.h */,
+ EDB1C0BE1653283D0041897E /* FImmutableSortedDictionaryTests.m */,
+ EDB1C0B81653283D0041897E /* Supporting Files */,
+ );
+ path = FImmutableSortedDictionaryTests;
+ sourceTree = "<group>";
+ };
+ EDB1C0B81653283D0041897E /* Supporting Files */ = {
+ isa = PBXGroup;
+ children = (
+ EDB1C0B91653283D0041897E /* FImmutableSortedDictionaryTests-Info.plist */,
+ EDB1C0BA1653283D0041897E /* InfoPlist.strings */,
+ );
+ name = "Supporting Files";
+ sourceTree = "<group>";
+ };
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+ EDB1C09C1653283D0041897E /* FImmutableSortedDictionary */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = EDB1C0C21653283D0041897E /* Build configuration list for PBXNativeTarget "FImmutableSortedDictionary" */;
+ buildPhases = (
+ EDB1C0991653283D0041897E /* Sources */,
+ EDB1C09A1653283D0041897E /* Frameworks */,
+ EDB1C09B1653283D0041897E /* CopyFiles */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ name = FImmutableSortedDictionary;
+ productName = FImmutableSortedDictionary;
+ productReference = EDB1C09D1653283D0041897E /* libFImmutableSortedDictionary.a */;
+ productType = "com.apple.product-type.library.static";
+ };
+ EDB1C0AD1653283D0041897E /* FImmutableSortedDictionaryTests */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = EDB1C0C51653283D0041897E /* Build configuration list for PBXNativeTarget "FImmutableSortedDictionaryTests" */;
+ buildPhases = (
+ EDB1C0A91653283D0041897E /* Sources */,
+ EDB1C0AA1653283D0041897E /* Frameworks */,
+ EDB1C0AB1653283D0041897E /* Resources */,
+ EDB1C0AC1653283D0041897E /* ShellScript */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ EDB1C0B51653283D0041897E /* PBXTargetDependency */,
+ );
+ name = FImmutableSortedDictionaryTests;
+ productName = FImmutableSortedDictionaryTests;
+ productReference = EDB1C0AE1653283D0041897E /* FImmutableSortedDictionaryTests.octest */;
+ productType = "com.apple.product-type.bundle";
+ };
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+ EDB1C0941653283D0041897E /* Project object */ = {
+ isa = PBXProject;
+ attributes = {
+ LastUpgradeCheck = 0450;
+ ORGANIZATIONNAME = Firebase;
+ };
+ buildConfigurationList = EDB1C0971653283D0041897E /* Build configuration list for PBXProject "FImmutableSortedDictionary" */;
+ compatibilityVersion = "Xcode 3.2";
+ developmentRegion = English;
+ hasScannedForEncodings = 0;
+ knownRegions = (
+ en,
+ );
+ mainGroup = EDB1C0921653283D0041897E;
+ productRefGroup = EDB1C09E1653283D0041897E /* Products */;
+ projectDirPath = "";
+ projectRoot = "";
+ targets = (
+ EDB1C09C1653283D0041897E /* FImmutableSortedDictionary */,
+ EDB1C0AD1653283D0041897E /* FImmutableSortedDictionaryTests */,
+ );
+ };
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+ EDB1C0AB1653283D0041897E /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ EDB1C0BC1653283D0041897E /* InfoPlist.strings in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXShellScriptBuildPhase section */
+ EDB1C0AC1653283D0041897E /* ShellScript */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "# Run the unit tests in this test bundle.\n\"${SYSTEM_DEVELOPER_DIR}/Tools/RunUnitTests\"\n";
+ };
+/* End PBXShellScriptBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+ EDB1C0991653283D0041897E /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ EDB1C0D21653286B0041897E /* FImmutableSortedDictionary.m in Sources */,
+ EDB1C0D41653286B0041897E /* FLLRBEmptyNode.m in Sources */,
+ EDB1C0D61653286B0041897E /* FLLRBValueNode.m in Sources */,
+ EDB1C0ED165331140041897E /* FImmutableSortedDictionaryEnumerator.m in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ EDB1C0A91653283D0041897E /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ EDB1C0BF1653283D0041897E /* FImmutableSortedDictionaryTests.m in Sources */,
+ EDB1C0D31653286B0041897E /* FImmutableSortedDictionary.m in Sources */,
+ EDB1C0D51653286B0041897E /* FLLRBEmptyNode.m in Sources */,
+ EDB1C0D71653286B0041897E /* FLLRBValueNode.m in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXSourcesBuildPhase section */
+
+/* Begin PBXTargetDependency section */
+ EDB1C0B51653283D0041897E /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = EDB1C09C1653283D0041897E /* FImmutableSortedDictionary */;
+ targetProxy = EDB1C0B41653283D0041897E /* PBXContainerItemProxy */;
+ };
+/* End PBXTargetDependency section */
+
+/* Begin PBXVariantGroup section */
+ EDB1C0BA1653283D0041897E /* InfoPlist.strings */ = {
+ isa = PBXVariantGroup;
+ children = (
+ EDB1C0BB1653283D0041897E /* en */,
+ );
+ name = InfoPlist.strings;
+ sourceTree = "<group>";
+ };
+/* End PBXVariantGroup section */
+
+/* Begin XCBuildConfiguration section */
+ EDB1C0C01653283D0041897E /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ COPY_PHASE_STRIP = NO;
+ GCC_C_LANGUAGE_STANDARD = gnu99;
+ GCC_DYNAMIC_NO_PIC = NO;
+ GCC_OPTIMIZATION_LEVEL = 0;
+ GCC_PREPROCESSOR_DEFINITIONS = (
+ "DEBUG=1",
+ "$(inherited)",
+ );
+ GCC_SYMBOLS_PRIVATE_EXTERN = NO;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 6.0;
+ ONLY_ACTIVE_ARCH = YES;
+ SDKROOT = iphoneos;
+ };
+ name = Debug;
+ };
+ EDB1C0C11653283D0041897E /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ COPY_PHASE_STRIP = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu99;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 6.0;
+ SDKROOT = iphoneos;
+ VALIDATE_PRODUCT = YES;
+ };
+ name = Release;
+ };
+ EDB1C0C31653283D0041897E /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ DSTROOT = /tmp/FImmutableSortedDictionary.dst;
+ GCC_PRECOMPILE_PREFIX_HEADER = YES;
+ GCC_PREFIX_HEADER = "FImmutableSortedDictionary/FImmutableSortedDictionary-Prefix.pch";
+ OTHER_LDFLAGS = "-ObjC";
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SKIP_INSTALL = YES;
+ };
+ name = Debug;
+ };
+ EDB1C0C41653283D0041897E /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ DSTROOT = /tmp/FImmutableSortedDictionary.dst;
+ GCC_PRECOMPILE_PREFIX_HEADER = YES;
+ GCC_PREFIX_HEADER = "FImmutableSortedDictionary/FImmutableSortedDictionary-Prefix.pch";
+ OTHER_LDFLAGS = "-ObjC";
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SKIP_INSTALL = YES;
+ };
+ name = Release;
+ };
+ EDB1C0C61653283D0041897E /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ FRAMEWORK_SEARCH_PATHS = (
+ "\"$(SDKROOT)/Developer/Library/Frameworks\"",
+ "\"$(DEVELOPER_LIBRARY_DIR)/Frameworks\"",
+ );
+ GCC_PRECOMPILE_PREFIX_HEADER = YES;
+ GCC_PREFIX_HEADER = "FImmutableSortedDictionary/FImmutableSortedDictionary-Prefix.pch";
+ INFOPLIST_FILE = "FImmutableSortedDictionaryTests/FImmutableSortedDictionaryTests-Info.plist";
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ WRAPPER_EXTENSION = octest;
+ };
+ name = Debug;
+ };
+ EDB1C0C71653283D0041897E /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ FRAMEWORK_SEARCH_PATHS = (
+ "\"$(SDKROOT)/Developer/Library/Frameworks\"",
+ "\"$(DEVELOPER_LIBRARY_DIR)/Frameworks\"",
+ );
+ GCC_PRECOMPILE_PREFIX_HEADER = YES;
+ GCC_PREFIX_HEADER = "FImmutableSortedDictionary/FImmutableSortedDictionary-Prefix.pch";
+ INFOPLIST_FILE = "FImmutableSortedDictionaryTests/FImmutableSortedDictionaryTests-Info.plist";
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ WRAPPER_EXTENSION = octest;
+ };
+ name = Release;
+ };
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+ EDB1C0971653283D0041897E /* Build configuration list for PBXProject "FImmutableSortedDictionary" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ EDB1C0C01653283D0041897E /* Debug */,
+ EDB1C0C11653283D0041897E /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ EDB1C0C21653283D0041897E /* Build configuration list for PBXNativeTarget "FImmutableSortedDictionary" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ EDB1C0C31653283D0041897E /* Debug */,
+ EDB1C0C41653283D0041897E /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ EDB1C0C51653283D0041897E /* Build configuration list for PBXNativeTarget "FImmutableSortedDictionaryTests" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ EDB1C0C61653283D0041897E /* Debug */,
+ EDB1C0C71653283D0041897E /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+/* End XCConfigurationList section */
+ };
+ rootObject = EDB1C0941653283D0041897E /* Project object */;
+}
diff --git a/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FArraySortedDictionary.h b/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FArraySortedDictionary.h
new file mode 100644
index 0000000..0c6c989
--- /dev/null
+++ b/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FArraySortedDictionary.h
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+#import "FImmutableSortedDictionary.h"
+
+/**
+ * This is an array backed implementation of FImmutableSortedDictionary. It uses arrays and linear lookups to achieve
+ * good memory efficiency while maintaining good performance for small collections. It also uses less allocations than
+ * a comparable red black tree. To avoid degrading performance with increasing collection size it will automatically
+ * convert to a FTreeSortedDictionary after an insert call above a certain threshold.
+ */
+@interface FArraySortedDictionary : FImmutableSortedDictionary
+
++ (FArraySortedDictionary *)fromDictionary:(NSDictionary *)dictionary withComparator:(NSComparator)comparator;
+
+- (id)initWithComparator:(NSComparator)comparator;
+
+#pragma mark -
+#pragma mark Properties
+
+@property (nonatomic, copy, readonly) NSComparator comparator;
+
+@end
diff --git a/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FArraySortedDictionary.m b/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FArraySortedDictionary.m
new file mode 100644
index 0000000..f572b6b
--- /dev/null
+++ b/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FArraySortedDictionary.m
@@ -0,0 +1,282 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FArraySortedDictionary.h"
+#import "FTreeSortedDictionary.h"
+
+@interface FArraySortedDictionaryEnumerator : NSEnumerator
+
+- (id)initWithKeys:(NSArray *)keys startPos:(NSInteger)pos isReverse:(BOOL)reverse;
+- (id)nextObject;
+
+@property (nonatomic) NSInteger pos;
+@property (nonatomic) BOOL reverse;
+@property (nonatomic, strong) NSArray *keys;
+
+@end
+
+@implementation FArraySortedDictionaryEnumerator
+
+- (id)initWithKeys:(NSArray *)keys startPos:(NSInteger)pos isReverse:(BOOL)reverse
+{
+ self = [super init];
+ if (self != nil) {
+ self->_pos = pos;
+ self->_reverse = reverse;
+ self->_keys = keys;
+ }
+ return self;
+}
+
+- (id)nextObject
+{
+ NSInteger pos = self->_pos;
+ if (pos >= 0 && pos < self.keys.count) {
+ if (self.reverse) {
+ self->_pos--;
+ } else {
+ self->_pos++;
+ }
+ return self.keys[pos];
+ } else {
+ return nil;
+ }
+}
+
+@end
+
+@interface FArraySortedDictionary ()
+
+- (id)initWithComparator:(NSComparator)comparator;
+
+@property (nonatomic, copy, readwrite) NSComparator comparator;
+@property (nonatomic, strong) NSArray *keys;
+@property (nonatomic, strong) NSArray *values;
+
+@end
+
+@implementation FArraySortedDictionary
+
++ (FArraySortedDictionary *)fromDictionary:(NSDictionary *)dictionary withComparator:(NSComparator)comparator
+{
+ NSMutableArray *keys = [NSMutableArray arrayWithCapacity:dictionary.count];
+ [dictionary enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
+ [keys addObject:key];
+ }];
+ [keys sortUsingComparator:comparator];
+
+ [keys enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
+ if (idx > 0) {
+ if (comparator(keys[idx - 1], obj) != NSOrderedAscending) {
+ [NSException raise:NSInvalidArgumentException format:@"Can't create FImmutableSortedDictionary with keys with same ordering!"];
+ }
+ }
+ }];
+
+ NSMutableArray *values = [NSMutableArray arrayWithCapacity:keys.count];
+ NSInteger pos = 0;
+ for (id key in keys) {
+ values[pos++] = dictionary[key];
+ }
+ NSAssert(values.count == keys.count, @"We added as many keys as values");
+ return [[FArraySortedDictionary alloc] initWithComparator:comparator keys:keys values:values];
+}
+
+- (id)initWithComparator:(NSComparator)comparator
+{
+ self = [super init];
+ if (self != nil) {
+ self->_comparator = comparator;
+ self->_keys = [NSArray array];
+ self->_values = [NSArray array];
+ }
+ return self;
+}
+
+- (id)initWithComparator:(NSComparator)comparator keys:(NSArray *)keys values:(NSArray *)values
+{
+ self = [super init];
+ if (self != nil) {
+ self->_comparator = comparator;
+ self->_keys = keys;
+ self->_values = values;
+ }
+ return self;
+}
+
+- (NSInteger) findInsertPositionForKey:(id)key
+{
+ NSInteger newPos = 0;
+ while (newPos < self.keys.count && self.comparator(self.keys[newPos], key) < NSOrderedSame) {
+ newPos++;
+ }
+ return newPos;
+}
+
+- (NSInteger) findKey:(id)key
+{
+ if (key == nil) {
+ return NSNotFound;
+ }
+ for (NSInteger pos = 0; pos < self.keys.count; pos++) {
+ NSComparisonResult result = self.comparator(key, self.keys[pos]);
+ if (result == NSOrderedSame) {
+ return pos;
+ } else if (result == NSOrderedAscending) {
+ return NSNotFound;
+ }
+ }
+ return NSNotFound;
+}
+
+- (FImmutableSortedDictionary *) insertKey:(id)key withValue:(id)value
+{
+ NSInteger pos = [self findKey:key];
+
+ if (pos == NSNotFound) {
+ /*
+ * If we're above the threshold we want to convert it to a tree backed implementation to not have
+ * degrading performance
+ */
+ if (self.count >= SORTED_DICTIONARY_ARRAY_TO_RB_TREE_SIZE_THRESHOLD) {
+ NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithCapacity:self.count];
+ for (NSInteger i = 0; i < self.keys.count; i++) {
+ dict[self.keys[i]] = self.values[i];
+ }
+ dict[key] = value;
+ return [FTreeSortedDictionary fromDictionary:dict withComparator:self.comparator];
+ } else {
+ NSMutableArray *newKeys = [NSMutableArray arrayWithArray:self.keys];
+ NSMutableArray *newValues = [NSMutableArray arrayWithArray:self.values];
+ NSInteger newPos = [self findInsertPositionForKey:key];
+ [newKeys insertObject:key atIndex:newPos];
+ [newValues insertObject:value atIndex:newPos];
+ return [[FArraySortedDictionary alloc] initWithComparator:self.comparator keys:newKeys values:newValues];
+ }
+ } else {
+ NSMutableArray *newKeys = [NSMutableArray arrayWithArray:self.keys];
+ NSMutableArray *newValues = [NSMutableArray arrayWithArray:self.values];
+ newKeys[pos] = key;
+ newValues[pos] = value;
+ return [[FArraySortedDictionary alloc] initWithComparator:self.comparator keys:newKeys values:newValues];
+ }
+}
+
+- (FImmutableSortedDictionary *) removeKey:(id)key
+{
+ NSInteger pos = [self findKey:key];
+ if (pos == NSNotFound) {
+ return self;
+ } else {
+ NSMutableArray *newKeys = [NSMutableArray arrayWithArray:self.keys];
+ NSMutableArray *newValues = [NSMutableArray arrayWithArray:self.values];
+ [newKeys removeObjectAtIndex:pos];
+ [newValues removeObjectAtIndex:pos];
+ return [[FArraySortedDictionary alloc] initWithComparator:self.comparator keys:newKeys values:newValues];
+ }
+}
+
+- (id) get:(id)key
+{
+ NSInteger pos = [self findKey:key];
+ if (pos == NSNotFound) {
+ return nil;
+ } else {
+ return self.values[pos];
+ }
+}
+
+- (id) getPredecessorKey:(id) key {
+ NSInteger pos = [self findKey:key];
+ if (pos == NSNotFound) {
+ [NSException raise:NSInternalInconsistencyException format:@"Can't get predecessor key for non-existent key"];
+ return nil;
+ } else if (pos == 0) {
+ return nil;
+ } else {
+ return self.keys[pos - 1];
+ }
+}
+
+- (BOOL) isEmpty {
+ return self.keys.count == 0;
+}
+
+- (int) count
+{
+ return (int)self.keys.count;
+}
+
+- (id) minKey
+{
+ return [self.keys firstObject];
+}
+
+- (id) maxKey
+{
+ return [self.keys lastObject];
+}
+
+- (void) enumerateKeysAndObjectsUsingBlock:(void (^)(id, id, BOOL *))block
+{
+ [self enumerateKeysAndObjectsReverse:NO usingBlock:block];
+}
+
+- (void) enumerateKeysAndObjectsReverse:(BOOL)reverse usingBlock:(void (^)(id, id, BOOL *))block
+{
+ if (reverse) {
+ BOOL stop = NO;
+ for (NSInteger i = self.keys.count - 1; i >= 0; i--) {
+ block(self.keys[i], self.values[i], &stop);
+ if (stop) return;
+ }
+ } else {
+ BOOL stop = NO;
+ for (NSInteger i = 0; i < self.keys.count; i++) {
+ block(self.keys[i], self.values[i], &stop);
+ if (stop) return;
+ }
+ }
+}
+
+- (BOOL) contains:(id)key {
+ return [self findKey:key] != NSNotFound;
+}
+
+- (NSEnumerator *) keyEnumerator {
+ return [self.keys objectEnumerator];
+}
+
+- (NSEnumerator *) keyEnumeratorFrom:(id)startKey {
+ NSInteger startPos = [self findInsertPositionForKey:startKey];
+ return [[FArraySortedDictionaryEnumerator alloc] initWithKeys:self.keys startPos:startPos isReverse:NO];
+}
+
+- (NSEnumerator *) reverseKeyEnumerator {
+ return [self.keys reverseObjectEnumerator];
+}
+
+- (NSEnumerator *) reverseKeyEnumeratorFrom:(id)startKey {
+ NSInteger startPos = [self findInsertPositionForKey:startKey];
+ // if there's no exact match, findKeyOrInsertPosition will return the index *after* the closest match, but
+ // since this is a reverse iterator, we want to start just *before* the closest match.
+ if (startPos >= self.keys.count || self.comparator(self.keys[startPos], startKey) != NSOrderedSame) {
+ startPos -= 1;
+ }
+ return [[FArraySortedDictionaryEnumerator alloc] initWithKeys:self.keys startPos:startPos isReverse:YES];
+}
+
+@end
diff --git a/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FImmutableSortedDictionary-Prefix.pch b/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FImmutableSortedDictionary-Prefix.pch
new file mode 100644
index 0000000..88d2408
--- /dev/null
+++ b/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FImmutableSortedDictionary-Prefix.pch
@@ -0,0 +1,7 @@
+//
+// Prefix header for all source files of the 'FImmutableSortedDictionary' target in the 'FImmutableSortedDictionary' project
+//
+
+#ifdef __OBJC__
+ #import <Foundation/Foundation.h>
+#endif
diff --git a/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FImmutableSortedDictionary.h b/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FImmutableSortedDictionary.h
new file mode 100644
index 0000000..1e7e5a3
--- /dev/null
+++ b/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FImmutableSortedDictionary.h
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+/**
+ * @fileoverview Implementation of an immutable SortedMap using a Left-leaning
+ * Red-Black Tree, adapted from the implementation in Mugs
+ * (http://mads379.github.com/mugs/) by Mads Hartmann Jensen
+ * (mads379@gmail.com).
+ *
+ * Original paper on Left-leaning Red-Black Trees:
+ * http://www.cs.princeton.edu/~rs/talks/LLRB/LLRB.pdf
+ *
+ * Invariant 1: No red node has a red child
+ * Invariant 2: Every leaf path has the same number of black nodes
+ * Invariant 3: Only the left child can be red (left leaning)
+ */
+
+#import <Foundation/Foundation.h>
+
+/**
+ * The size threshold where we use a tree backed sorted map instead of an array backed sorted map.
+ * This is a more or less arbitrary chosen value, that was chosen to be large enough to fit most of object kind
+ * of Firebase data, but small enough to not notice degradation in performance for inserting and lookups.
+ * Feel free to empirically determine this constant, but don't expect much gain in real world performance.
+ */
+#define SORTED_DICTIONARY_ARRAY_TO_RB_TREE_SIZE_THRESHOLD 25
+
+@interface FImmutableSortedDictionary : NSObject
+
++ (FImmutableSortedDictionary *)dictionaryWithComparator:(NSComparator)comparator;
++ (FImmutableSortedDictionary *)fromDictionary:(NSDictionary *)dictionary withComparator:(NSComparator)comparator;
+
+- (FImmutableSortedDictionary *) insertKey:(id)aKey withValue:(id)aValue;
+- (FImmutableSortedDictionary *) removeKey:(id)aKey;
+- (id) get:(id) key;
+- (id) getPredecessorKey:(id) key;
+- (BOOL) isEmpty;
+- (int) count;
+- (id) minKey;
+- (id) maxKey;
+- (void) enumerateKeysAndObjectsUsingBlock:(void(^)(id key, id value, BOOL *stop))block;
+- (void) enumerateKeysAndObjectsReverse:(BOOL)reverse usingBlock:(void(^)(id key, id value, BOOL *stop))block;
+- (BOOL) contains:(id)key;
+- (NSEnumerator *) keyEnumerator;
+- (NSEnumerator *) keyEnumeratorFrom:(id)startKey;
+- (NSEnumerator *) reverseKeyEnumerator;
+- (NSEnumerator *) reverseKeyEnumeratorFrom:(id)startKey;
+
+#pragma mark -
+#pragma mark Methods similar to NSMutableDictionary
+
+- (FImmutableSortedDictionary *) setObject:(id)anObject forKey:(id)aKey;
+- (id) objectForKey:(id)key;
+- (FImmutableSortedDictionary *) removeObjectForKey:(id)aKey;
+
+@end
+
diff --git a/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FImmutableSortedDictionary.m b/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FImmutableSortedDictionary.m
new file mode 100644
index 0000000..006c12d
--- /dev/null
+++ b/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FImmutableSortedDictionary.m
@@ -0,0 +1,158 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FImmutableSortedDictionary.h"
+#import "FArraySortedDictionary.h"
+#import "FTreeSortedDictionary.h"
+
+#define THROW_ABSTRACT_METHOD_EXCEPTION(sel) do { \
+ @throw [NSException exceptionWithName:NSInternalInconsistencyException \
+ reason:[NSString stringWithFormat:@"You must override %@ in a subclass", NSStringFromSelector(sel)] \
+ userInfo:nil]; \
+} while(0)
+
+@implementation FImmutableSortedDictionary
+
++ (FImmutableSortedDictionary *)dictionaryWithComparator:(NSComparator)comparator
+{
+ return [[FArraySortedDictionary alloc] initWithComparator:comparator];
+}
+
++ (FImmutableSortedDictionary *)fromDictionary:(NSDictionary *)dictionary withComparator:(NSComparator)comparator
+{
+ if (dictionary.count <= SORTED_DICTIONARY_ARRAY_TO_RB_TREE_SIZE_THRESHOLD) {
+ return [FArraySortedDictionary fromDictionary:dictionary withComparator:comparator];
+ } else {
+ return [FTreeSortedDictionary fromDictionary:dictionary withComparator:comparator];
+ }
+}
+
+- (FImmutableSortedDictionary *) insertKey:(id)aKey withValue:(id)aValue {
+ THROW_ABSTRACT_METHOD_EXCEPTION(@selector(insertKey:withValue:));
+}
+
+- (FImmutableSortedDictionary *) removeKey:(id)aKey {
+ THROW_ABSTRACT_METHOD_EXCEPTION(@selector(removeKey:));
+}
+
+- (id) get:(id) key {
+ THROW_ABSTRACT_METHOD_EXCEPTION(@selector(get:));
+}
+
+- (id) getPredecessorKey:(id) key {
+ THROW_ABSTRACT_METHOD_EXCEPTION(@selector(getPredecessorKey:));
+}
+
+- (BOOL) isEmpty {
+ THROW_ABSTRACT_METHOD_EXCEPTION(@selector(isEmpty));
+}
+
+- (int) count {
+ THROW_ABSTRACT_METHOD_EXCEPTION(@selector((count)));
+}
+
+- (id) minKey {
+ THROW_ABSTRACT_METHOD_EXCEPTION(@selector(minKey));
+}
+
+- (id) maxKey {
+ THROW_ABSTRACT_METHOD_EXCEPTION(@selector(maxKey));
+}
+
+- (void) enumerateKeysAndObjectsUsingBlock:(void (^)(id, id, BOOL *))block {
+ THROW_ABSTRACT_METHOD_EXCEPTION(@selector(enumerateKeysAndObjectsUsingBlock:));
+}
+
+- (void) enumerateKeysAndObjectsReverse:(BOOL)reverse usingBlock:(void (^)(id, id, BOOL *))block {
+ THROW_ABSTRACT_METHOD_EXCEPTION(@selector(enumerateKeysAndObjectsReverse:usingBlock:));
+}
+
+- (BOOL) contains:(id)key {
+ THROW_ABSTRACT_METHOD_EXCEPTION(@selector(contains:));
+}
+
+- (NSEnumerator *) keyEnumerator {
+ THROW_ABSTRACT_METHOD_EXCEPTION(@selector(keyEnumerator));
+}
+
+- (NSEnumerator *) keyEnumeratorFrom:(id)startKey {
+ THROW_ABSTRACT_METHOD_EXCEPTION(@selector(keyEnumeratorFrom:));
+}
+
+- (NSEnumerator *) reverseKeyEnumerator {
+ THROW_ABSTRACT_METHOD_EXCEPTION(@selector(reverseKeyEnumerator));
+}
+
+- (NSEnumerator *) reverseKeyEnumeratorFrom:(id)startKey {
+ THROW_ABSTRACT_METHOD_EXCEPTION(@selector(reverseKeyEnumeratorFrom:));
+}
+
+- (BOOL)isEqual:(id)object {
+ if (![object isKindOfClass:[FImmutableSortedDictionary class]]) {
+ return NO;
+ }
+ FImmutableSortedDictionary *other = (FImmutableSortedDictionary *)object;
+ if (self.count != other.count) {
+ return NO;
+ }
+ __block BOOL isEqual = YES;
+ [self enumerateKeysAndObjectsUsingBlock:^(id key, id value, BOOL *stop) {
+ id otherValue = [other objectForKey:key];
+ isEqual = isEqual && (value == otherValue || [value isEqual:otherValue]);
+ *stop = !isEqual;
+ }];
+ return isEqual;
+}
+
+- (NSUInteger)hash {
+ __block NSUInteger hash = 0;
+ [self enumerateKeysAndObjectsUsingBlock:^(id key, id value, BOOL *stop) {
+ hash = (hash * 31 + [key hash]) * 17 + [value hash];
+ }];
+ return hash;
+}
+
+- (NSString *)description {
+ NSMutableString *str = [[NSMutableString alloc] init];
+ __block BOOL first = YES;
+ [str appendString:@"{ "];
+ [self enumerateKeysAndObjectsUsingBlock:^(id key, id value, BOOL *stop) {
+ if (!first) {
+ [str appendString:@", "];
+ }
+ first = NO;
+ [str appendString:[NSString stringWithFormat:@"%@: %@", key, value]];
+ }];
+ [str appendString:@" }"];
+ return str;
+}
+
+#pragma mark -
+#pragma mark Methods similar to NSMutableDictionary
+
+- (FImmutableSortedDictionary *) setObject:(__unsafe_unretained id)anObject forKey:(__unsafe_unretained id)aKey {
+ return [self insertKey:aKey withValue:anObject];
+}
+
+- (FImmutableSortedDictionary *) removeObjectForKey:(__unsafe_unretained id)aKey {
+ return [self removeKey:aKey];
+}
+
+- (id) objectForKey:(__unsafe_unretained id)key {
+ return [self get:key];
+}
+
+@end
diff --git a/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FImmutableSortedSet.h b/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FImmutableSortedSet.h
new file mode 100644
index 0000000..bb1a39c
--- /dev/null
+++ b/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FImmutableSortedSet.h
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+@interface FImmutableSortedSet : NSObject
+
++ (FImmutableSortedSet *)setWithKeysFromDictionary:(NSDictionary *)array comparator:(NSComparator)comparator;
+
+- (BOOL)containsObject:(id)object;
+- (FImmutableSortedSet *)addObject:(id)object;
+- (FImmutableSortedSet *)removeObject:(id)object;
+- (id)firstObject;
+- (id)lastObject;
+- (NSUInteger)count;
+- (BOOL)isEmpty;
+
+- (id)predecessorEntry:(id)entry;
+
+- (void)enumerateObjectsUsingBlock:(void (^)(id obj, BOOL *stop))block;
+- (void)enumerateObjectsReverse:(BOOL)reverse usingBlock:(void (^)(id obj, BOOL *stop))block;
+
+- (NSEnumerator *)objectEnumerator;
+
+@end
diff --git a/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FImmutableSortedSet.m b/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FImmutableSortedSet.m
new file mode 100644
index 0000000..09c4164
--- /dev/null
+++ b/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FImmutableSortedSet.m
@@ -0,0 +1,131 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FImmutableSortedSet.h"
+#import "FImmutableSortedDictionary.h"
+
+@interface FImmutableSortedSet ()
+
+@property (nonatomic, strong) FImmutableSortedDictionary *dictionary;
+
+@end
+
+@implementation FImmutableSortedSet
+
++ (FImmutableSortedSet *)setWithKeysFromDictionary:(NSDictionary *)dictionary comparator:(NSComparator)comparator
+{
+ FImmutableSortedDictionary *setDict = [FImmutableSortedDictionary fromDictionary:dictionary withComparator:comparator];
+ return [[FImmutableSortedSet alloc] initWithDictionary:setDict];
+}
+
+- (id)initWithDictionary:(FImmutableSortedDictionary *)dictionary
+{
+ self = [super init];
+ if (self != nil) {
+ self->_dictionary = dictionary;
+ }
+ return self;
+}
+
+- (BOOL)contains:(id)object
+{
+ return [self.dictionary contains:object];
+}
+
+- (FImmutableSortedSet *)addObject:(id)object
+{
+ FImmutableSortedDictionary *newDictionary = [self.dictionary insertKey:object withValue:[NSNull null]];
+ if (newDictionary != self.dictionary) {
+ return [[FImmutableSortedSet alloc] initWithDictionary:newDictionary];
+ } else {
+ return self;
+ }
+}
+
+- (FImmutableSortedSet *)removeObject:(id)object
+{
+ FImmutableSortedDictionary *newDictionary = [self.dictionary removeObjectForKey:object];
+ if (newDictionary != self.dictionary) {
+ return [[FImmutableSortedSet alloc] initWithDictionary:newDictionary];
+ } else {
+ return self;
+ }
+}
+
+- (BOOL)containsObject:(id)object
+{
+ return [self.dictionary contains:object];
+}
+
+- (id)firstObject
+{
+ return [self.dictionary minKey];
+}
+
+- (id)lastObject
+{
+ return [self.dictionary maxKey];
+}
+
+- (id)predecessorEntry:(id)entry
+{
+ return [self.dictionary getPredecessorKey:entry];
+}
+
+- (NSUInteger)count
+{
+ return [self.dictionary count];
+}
+
+- (BOOL)isEmpty
+{
+ return [self.dictionary isEmpty];
+}
+
+- (void)enumerateObjectsUsingBlock:(void (^)(id, BOOL *))block
+{
+ [self enumerateObjectsReverse:NO usingBlock:block];
+}
+
+- (void)enumerateObjectsReverse:(BOOL)reverse usingBlock:(void (^)(id, BOOL *))block
+{
+ [self.dictionary enumerateKeysAndObjectsReverse:reverse usingBlock:^(id key, id value, BOOL *stop) {
+ block(key, stop);
+ }];
+}
+
+- (NSEnumerator *)objectEnumerator
+{
+ return [self.dictionary keyEnumerator];
+}
+
+- (NSString *)description
+{
+ NSMutableString *str = [[NSMutableString alloc] init];
+ __block BOOL first = YES;
+ [str appendString:@"FImmutableSortedSet ( "];
+ [self enumerateObjectsUsingBlock:^(id obj, BOOL *stop) {
+ if (!first) {
+ [str appendString:@", "];
+ }
+ first = NO;
+ [str appendString:[NSString stringWithFormat:@"%@", obj]];
+ }];
+ [str appendString:@" )"];
+ return str;
+}
+
+@end
diff --git a/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FLLRBEmptyNode.h b/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FLLRBEmptyNode.h
new file mode 100644
index 0000000..3257447
--- /dev/null
+++ b/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FLLRBEmptyNode.h
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+#import "FLLRBNode.h"
+
+@interface FLLRBEmptyNode : NSObject <FLLRBNode>
+
++ (id)emptyNode;
+
+- (id)copyWith:(id) aKey withValue:(id) aValue withColor:(FLLRBColor*) aColor withLeft:(id<FLLRBNode>)aLeft withRight:(id<FLLRBNode>)aRight;
+- (id<FLLRBNode>) insertKey:(id) aKey forValue:(id)aValue withComparator:(NSComparator)aComparator;
+- (id<FLLRBNode>) remove:(id) aKey withComparator:(NSComparator)aComparator;
+- (int) count;
+- (BOOL) isEmpty;
+- (BOOL) inorderTraversal:(BOOL (^)(id key, id value))action;
+- (BOOL) reverseTraversal:(BOOL (^)(id key, id value))action;
+- (id<FLLRBNode>) min;
+- (id) minKey;
+- (id) maxKey;
+- (BOOL) isRed;
+- (int) check;
+
+@property (nonatomic, strong) id key;
+@property (nonatomic, strong) id value;
+@property (nonatomic, strong) FLLRBColor* color;
+@property (nonatomic, strong) id<FLLRBNode> left;
+@property (nonatomic, strong) id<FLLRBNode> right;
+
+@end
diff --git a/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FLLRBEmptyNode.m b/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FLLRBEmptyNode.m
new file mode 100644
index 0000000..adbc6ca
--- /dev/null
+++ b/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FLLRBEmptyNode.m
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FLLRBEmptyNode.h"
+#import "FLLRBValueNode.h"
+
+@implementation FLLRBEmptyNode
+
+@synthesize key, value, color, left, right;
+
+- (NSString *) description {
+ return [NSString stringWithFormat:@"[key=%@ val=%@ color=%@]", key, value, (color ? @"true" : @"false")];
+}
+
++ (id)emptyNode
+{
+ static dispatch_once_t pred = 0;
+ __strong static id _sharedObject = nil;
+ dispatch_once(&pred, ^{
+ _sharedObject = [[self alloc] init]; // or some other init method
+ });
+ return _sharedObject;
+}
+
+- (id)copyWith:(id) aKey withValue:(id) aValue withColor:(FLLRBColor*) aColor withLeft:(id<FLLRBNode>)aLeft withRight:(id<FLLRBNode>)aRight {
+ return self;
+}
+
+- (id<FLLRBNode>) insertKey:(id) aKey forValue:(id)aValue withComparator:(NSComparator)aComparator {
+ FLLRBValueNode* result = [[FLLRBValueNode alloc] initWithKey:aKey withValue:aValue withColor:nil withLeft:nil withRight:nil];
+ return result;
+}
+
+- (id<FLLRBNode>) remove:(id) key withComparator:(NSComparator)aComparator {
+ return self;
+}
+
+- (int) count {
+ return 0;
+}
+
+- (BOOL) isEmpty {
+ return YES;
+}
+
+- (BOOL) inorderTraversal:(BOOL (^)(id key, id value))action {
+ return NO;
+}
+
+- (BOOL) reverseTraversal:(BOOL (^)(id key, id value))action {
+ return NO;
+}
+
+- (id<FLLRBNode>) min {
+ return self;
+}
+
+- (id) minKey {
+ return nil;
+}
+
+- (id) maxKey {
+ return nil;
+}
+
+- (BOOL) isRed {
+ return NO;
+}
+
+- (int) check {
+ return 0;
+}
+
+@end
diff --git a/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FLLRBNode.h b/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FLLRBNode.h
new file mode 100644
index 0000000..2634494
--- /dev/null
+++ b/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FLLRBNode.h
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+#define RED @true
+#define BLACK @false
+
+typedef NSNumber FLLRBColor;
+
+@protocol FLLRBNode <NSObject>
+
+- (id)copyWith:(id) aKey withValue:(id) aValue withColor:(FLLRBColor*) aColor withLeft:(id<FLLRBNode>)aLeft withRight:(id<FLLRBNode>)aRight;
+- (id<FLLRBNode>) insertKey:(id) aKey forValue:(id)aValue withComparator:(NSComparator)aComparator;
+- (id<FLLRBNode>) remove:(id) key withComparator:(NSComparator)aComparator;
+- (int) count;
+- (BOOL) isEmpty;
+- (BOOL) inorderTraversal:(BOOL (^)(id key, id value))action;
+- (BOOL) reverseTraversal:(BOOL (^)(id key, id value))action;
+- (id<FLLRBNode>) min;
+- (id) minKey;
+- (id) maxKey;
+- (BOOL) isRed;
+- (int) check;
+
+@property (nonatomic, strong) id key;
+@property (nonatomic, strong) id value;
+@property (nonatomic, strong) FLLRBColor* color;
+@property (nonatomic, strong) id<FLLRBNode> left;
+@property (nonatomic, strong) id<FLLRBNode> right;
+
+@end
diff --git a/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FLLRBValueNode.h b/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FLLRBValueNode.h
new file mode 100644
index 0000000..2c64b8a
--- /dev/null
+++ b/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FLLRBValueNode.h
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+#import "FLLRBNode.h"
+
+@interface FLLRBValueNode : NSObject <FLLRBNode>
+
+
+- (id)initWithKey:(id) key withValue:(id) value withColor:(FLLRBColor*) color withLeft:(id<FLLRBNode>)left withRight:(id<FLLRBNode>)right;
+- (id)copyWith:(id) aKey withValue:(id) aValue withColor:(FLLRBColor*) aColor withLeft:(id<FLLRBNode>)aLeft withRight:(id<FLLRBNode>)aRight;
+- (id<FLLRBNode>) insertKey:(id) aKey forValue:(id)aValue withComparator:(NSComparator)aComparator;
+- (id<FLLRBNode>) remove:(id) aKey withComparator:(NSComparator)aComparator;
+- (int) count;
+- (BOOL) isEmpty;
+- (BOOL) inorderTraversal:(BOOL (^)(id key, id value))action;
+- (BOOL) reverseTraversal:(BOOL (^)(id key, id value))action;
+- (id<FLLRBNode>) min;
+- (id) minKey;
+- (id) maxKey;
+- (BOOL) isRed;
+- (int) check;
+
+- (BOOL) checkMaxDepth;
+
+@property (nonatomic, strong) id key;
+@property (nonatomic, strong) id value;
+@property (nonatomic, strong) FLLRBColor* color;
+@property (nonatomic, strong) id<FLLRBNode> left;
+@property (nonatomic, strong) id<FLLRBNode> right;
+
+@end
diff --git a/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FLLRBValueNode.m b/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FLLRBValueNode.m
new file mode 100644
index 0000000..f361278
--- /dev/null
+++ b/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FLLRBValueNode.m
@@ -0,0 +1,245 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FLLRBValueNode.h"
+#import "FLLRBEmptyNode.h"
+
+@implementation FLLRBValueNode
+
+@synthesize key, value, color, left, right;
+
+- (NSString *) description {
+ return [NSString stringWithFormat:@"[key=%@ val=%@ color=%@]", key, value, (color ? @"true" : @"false")];
+}
+
+- (id)initWithKey:(__unsafe_unretained id) aKey withValue:(__unsafe_unretained id) aValue withColor:(__unsafe_unretained FLLRBColor*) aColor withLeft:(__unsafe_unretained id<FLLRBNode>)aLeft withRight:(__unsafe_unretained id<FLLRBNode>)aRight
+{
+ self = [super init];
+ if (self) {
+ self.key = aKey;
+ self.value = aValue;
+ self.color = aColor != nil ? aColor : RED;
+ self.left = aLeft != nil ? aLeft : [FLLRBEmptyNode emptyNode];
+ self.right = aRight != nil ? aRight : [FLLRBEmptyNode emptyNode];
+ }
+ return self;
+}
+
+- (id)copyWith:(__unsafe_unretained id) aKey withValue:(__unsafe_unretained id) aValue withColor:(__unsafe_unretained FLLRBColor*) aColor withLeft:(__unsafe_unretained id<FLLRBNode>)aLeft withRight:(__unsafe_unretained id<FLLRBNode>)aRight {
+ return [[FLLRBValueNode alloc] initWithKey:(aKey != nil) ? aKey : self.key
+ withValue:(aValue != nil) ? aValue : self.value
+ withColor:(aColor != nil) ? aColor : self.color
+ withLeft:(aLeft != nil) ? aLeft : self.left
+ withRight:(aRight != nil) ? aRight : self.right];
+}
+
+- (int) count {
+ return [self.left count] + 1 + [self.right count];
+}
+
+- (BOOL) isEmpty {
+ return NO;
+}
+
+/**
+* Early terminates if aciton returns YES.
+* @return The first truthy value returned by action, or the last falsey value returned by action.
+*/
+- (BOOL) inorderTraversal:(BOOL (^)(id key, id value))action {
+ return [self.left inorderTraversal:action] ||
+ action(self.key, self.value) ||
+ [self.right inorderTraversal:action];
+}
+
+- (BOOL) reverseTraversal:(BOOL (^)(id key, id value))action {
+ return [self.right reverseTraversal:action] ||
+ action(self.key, self.value) ||
+ [self.left reverseTraversal:action];
+}
+
+- (id<FLLRBNode>) min {
+ if([self.left isEmpty]) {
+ return self;
+ }
+ else {
+ return [self.left min];
+ }
+}
+
+- (id) minKey {
+ return [[self min] key];
+}
+
+- (id) maxKey {
+ if([self.right isEmpty]) {
+ return self.key;
+ }
+ else {
+ return [self.right maxKey];
+ }
+}
+
+- (id<FLLRBNode>) insertKey:(__unsafe_unretained id) aKey forValue:(__unsafe_unretained id)aValue withComparator:(NSComparator)aComparator {
+ NSComparisonResult cmp = aComparator(aKey, self.key);
+ FLLRBValueNode* n = self;
+
+ if(cmp == NSOrderedAscending) {
+ n = [n copyWith:nil withValue:nil withColor:nil withLeft:[n.left insertKey:aKey forValue:aValue withComparator:aComparator] withRight:nil];
+ }
+ else if(cmp == NSOrderedSame) {
+ n = [n copyWith:nil withValue:aValue withColor:nil withLeft:nil withRight:nil];
+ }
+ else {
+ n = [n copyWith:nil withValue:nil withColor:nil withLeft:nil withRight:[n.right insertKey:aKey forValue:aValue withComparator:aComparator]];
+ }
+
+ return [n fixUp];
+}
+
+- (id<FLLRBNode>) removeMin {
+
+ if([self.left isEmpty]) {
+ return [FLLRBEmptyNode emptyNode];
+ }
+
+ FLLRBValueNode* n = self;
+ if(! [n.left isRed] && ! [n.left.left isRed]) {
+ n = [n moveRedLeft];
+ }
+
+ n = [n copyWith:nil withValue:nil withColor:nil withLeft:[(FLLRBValueNode*)n.left removeMin] withRight:nil];
+ return [n fixUp];
+}
+
+
+- (id<FLLRBNode>) fixUp {
+ FLLRBValueNode* n = self;
+ if([n.right isRed] && ! [n.left isRed]) n = [n rotateLeft];
+ if([n.left isRed] && [n.left.left isRed]) n = [n rotateRight];
+ if([n.left isRed] && [n.right isRed]) n = [n colorFlip];
+ return n;
+}
+
+- (FLLRBValueNode*) moveRedLeft {
+ FLLRBValueNode* n = [self colorFlip];
+ if([n.right.left isRed]) {
+ n = [n copyWith:nil withValue:nil withColor:nil withLeft:nil withRight:[(FLLRBValueNode*)n.right rotateRight]];
+ n = [n rotateLeft];
+ n = [n colorFlip];
+ }
+ return n;
+}
+
+- (FLLRBValueNode*) moveRedRight {
+ FLLRBValueNode* n = [self colorFlip];
+ if([n.left.left isRed]) {
+ n = [n rotateRight];
+ n = [n colorFlip];
+ }
+ return n;
+}
+
+- (id<FLLRBNode>) rotateLeft {
+ id<FLLRBNode> nl = [self copyWith:nil withValue:nil withColor:RED withLeft:nil withRight:self.right.left];
+ return [self.right copyWith:nil withValue:nil withColor:self.color withLeft:nl withRight:nil];;
+}
+
+- (id<FLLRBNode>) rotateRight {
+ id<FLLRBNode> nr = [self copyWith:nil withValue:nil withColor:RED withLeft:self.left.right withRight:nil];
+ return [self.left copyWith:nil withValue:nil withColor:self.color withLeft:nil withRight:nr];
+}
+
+- (id<FLLRBNode>) colorFlip {
+ id<FLLRBNode> nleft = [self.left copyWith:nil withValue:nil withColor:[NSNumber numberWithBool:![self.left.color boolValue]] withLeft:nil withRight:nil];
+ id<FLLRBNode> nright = [self.right copyWith:nil withValue:nil withColor:[NSNumber numberWithBool:![self.right.color boolValue]] withLeft:nil withRight:nil];
+
+ return [self copyWith:nil withValue:nil withColor:[NSNumber numberWithBool:![self.color boolValue]] withLeft:nleft withRight:nright];
+}
+
+- (id<FLLRBNode>) remove:(__unsafe_unretained id) aKey withComparator:(NSComparator)comparator {
+ id<FLLRBNode> smallest;
+ FLLRBValueNode* n = self;
+
+ if(comparator(aKey, n.key) == NSOrderedAscending) {
+ if(![n.left isEmpty] && ![n.left isRed] && ![n.left.left isRed]) {
+ n = [n moveRedLeft];
+ }
+ n = [n copyWith:nil withValue:nil withColor:nil withLeft:[n.left remove:aKey withComparator:comparator] withRight:nil];
+ }
+ else {
+ if([n.left isRed]) {
+ n = [n rotateRight];
+ }
+
+ if(![n.right isEmpty] && ![n.right isRed] && ![n.right.left isRed]) {
+ n = [n moveRedRight];
+ }
+
+ if(comparator(aKey, n.key) == NSOrderedSame) {
+ if([n.right isEmpty]) {
+ return [FLLRBEmptyNode emptyNode];
+ }
+ else {
+ smallest = [n.right min];
+ n = [n copyWith:smallest.key withValue:smallest.value withColor:nil withLeft:nil withRight:[(FLLRBValueNode*)n.right removeMin]];
+ }
+ }
+ n = [n copyWith:nil withValue:nil withColor:nil withLeft:nil withRight:[n.right remove:aKey withComparator:comparator]];
+ }
+ return [n fixUp];
+}
+
+- (BOOL) isRed {
+ return [self.color boolValue];
+}
+
+- (BOOL) checkMaxDepth {
+ int blackDepth = [self check];
+ if(pow(2.0, blackDepth) <= ([self count] + 1)) {
+ return YES;
+ }
+ else {
+ return NO;
+ }
+}
+
+- (int) check {
+ int blackDepth = 0;
+
+ if([self isRed] && [self.left isRed]) {
+ @throw [[NSException alloc] initWithName:@"check" reason:@"Red node has a red child" userInfo:nil];
+ }
+
+ if([self.right isRed]) {
+ @throw [[NSException alloc] initWithName:@"check" reason:@"Right child is red" userInfo:nil];
+ }
+
+ blackDepth = [self.left check];
+// NSLog(err);
+ if(blackDepth != [self.right check]) {
+ NSString* err = [NSString stringWithFormat:@"(%@ -> %@)blackDepth: %d ; self.right check: %d", self.value, [self.color boolValue] ? @"red" : @"black", blackDepth, [self.right check]];
+// return 10;
+ @throw [[NSException alloc] initWithName:@"check" reason:err userInfo:nil];
+ }
+ else {
+ int ret = blackDepth + ([self isRed] ? 0 : 1);
+// NSLog(@"black depth is: %d; other is: %d, ret is: %d", blackDepth, ([self isRed] ? 0 : 1), ret);
+ return ret;
+ }
+}
+
+
+@end
diff --git a/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FTreeSortedDictionaryEnumerator.h b/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FTreeSortedDictionaryEnumerator.h
new file mode 100644
index 0000000..d7fe835
--- /dev/null
+++ b/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FTreeSortedDictionaryEnumerator.h
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+#import "FTreeSortedDictionary.h"
+
+@interface FTreeSortedDictionaryEnumerator : NSEnumerator
+
+- (id)initWithImmutableSortedDictionary:(FTreeSortedDictionary *)aDict startKey:(id)startKey isReverse:(BOOL)reverse;
+- (id)nextObject;
+
+@end
diff --git a/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FTreeSortedDictionaryEnumerator.m b/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FTreeSortedDictionaryEnumerator.m
new file mode 100644
index 0000000..6636d1e
--- /dev/null
+++ b/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FTreeSortedDictionaryEnumerator.m
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FTreeSortedDictionaryEnumerator.h"
+
+@interface FTreeSortedDictionaryEnumerator()
+@property (nonatomic, strong) FTreeSortedDictionary* immutableSortedDictionary;
+@property (nonatomic, strong) NSMutableArray* stack;
+@property (nonatomic) BOOL isReverse;
+
+@end
+
+@implementation FTreeSortedDictionaryEnumerator
+
+- (id)initWithImmutableSortedDictionary:(FTreeSortedDictionary *)aDict
+ startKey:(id)startKey isReverse:(BOOL)reverse {
+ self = [super init];
+ if (self) {
+ self.immutableSortedDictionary = aDict;
+ self.stack = [[NSMutableArray alloc] init];
+ self.isReverse = reverse;
+
+ NSComparator comparator = aDict.comparator;
+ id<FLLRBNode> node = self.immutableSortedDictionary.root;
+
+ NSInteger cmp;
+ while(![node isEmpty]) {
+ cmp = startKey ? comparator(node.key, startKey) : 1;
+ // flip the comparison if we're going in reverse
+ if (self.isReverse) cmp *= -1;
+
+ if (cmp < 0) {
+ // This node is less than our start key. Ignore it.
+ if (self.isReverse) {
+ node = node.left;
+ } else {
+ node = node.right;
+ }
+ } else if (cmp == 0) {
+ // This node is exactly equal to our start key. Push it on the stack, but stop iterating:
+ [self.stack addObject:node];
+ break;
+ } else {
+ // This node is greater than our start key, add it to the stack and move on to the next one.
+ [self.stack addObject:node];
+ if (self.isReverse) {
+ node = node.right;
+ } else {
+ node = node.left;
+ }
+ }
+ }
+ }
+ return self;
+}
+
+- (id)nextObject {
+ if([self.stack count] == 0) {
+ return nil;
+ }
+
+ id<FLLRBNode> node = nil;
+ @synchronized(self.stack) {
+ node = [self.stack lastObject];
+ [self.stack removeLastObject];
+ }
+ id result = node.key;
+
+ if (self.isReverse) {
+ node = node.left;
+ while (![node isEmpty]) {
+ [self.stack addObject:node];
+ node = node.right;
+ }
+ } else {
+ node = node.right;
+ while (![node isEmpty]) {
+ [self.stack addObject:node];
+ node = node.left;
+ }
+ }
+
+ return result;
+}
+
+@end
diff --git a/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionaryTests/FImmutableSortedDictionaryTests-Info.plist b/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionaryTests/FImmutableSortedDictionaryTests-Info.plist
new file mode 100644
index 0000000..42887ee
--- /dev/null
+++ b/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionaryTests/FImmutableSortedDictionaryTests-Info.plist
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>CFBundleDevelopmentRegion</key>
+ <string>en</string>
+ <key>CFBundleExecutable</key>
+ <string>${EXECUTABLE_NAME}</string>
+ <key>CFBundleIdentifier</key>
+ <string>com.firebase.mobile.ios.${PRODUCT_NAME:rfc1034identifier}</string>
+ <key>CFBundleInfoDictionaryVersion</key>
+ <string>6.0</string>
+ <key>CFBundlePackageType</key>
+ <string>BNDL</string>
+ <key>CFBundleShortVersionString</key>
+ <string>1.0</string>
+ <key>CFBundleSignature</key>
+ <string>????</string>
+ <key>CFBundleVersion</key>
+ <string>1</string>
+</dict>
+</plist>
diff --git a/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionaryTests/en.lproj/InfoPlist.strings b/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionaryTests/en.lproj/InfoPlist.strings
new file mode 100644
index 0000000..477b28f
--- /dev/null
+++ b/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionaryTests/en.lproj/InfoPlist.strings
@@ -0,0 +1,2 @@
+/* Localized versions of Info.plist keys */
+
diff --git a/Firebase/Database/FIndex.h b/Firebase/Database/FIndex.h
new file mode 100644
index 0000000..8ab08c8
--- /dev/null
+++ b/Firebase/Database/FIndex.h
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+@class FImmutableSortedDictionary;
+@class FNamedNode;
+@protocol FNode;
+
+@protocol FIndex<NSObject, NSCopying>
+- (NSComparisonResult) compareKey:(NSString *)key1
+ andNode:(id<FNode>)node1
+ toOtherKey:(NSString *)key2
+ andNode:(id<FNode>)node2;
+
+- (NSComparisonResult) compareKey:(NSString *)key1
+ andNode:(id<FNode>)node1
+ toOtherKey:(NSString *)key2
+ andNode:(id<FNode>)node2
+ reverse:(BOOL)reverse;
+
+- (NSComparisonResult) compareNamedNode:(FNamedNode *)namedNode1 toNamedNode:(FNamedNode *)namedNode2;
+
+- (BOOL) isDefinedOn:(id<FNode>)node;
+- (BOOL) indexedValueChangedBetween:(id<FNode>)oldNode and:(id<FNode>)newNode;
+- (FNamedNode*) minPost;
+- (FNamedNode*) maxPost;
+- (FNamedNode*) makePost:(id<FNode>)indexValue name:(NSString*)name;
+- (NSString*) queryDefinition;
+
+@end
+
+@interface FIndex : NSObject
+
++ (id<FIndex>)indexFromQueryDefinition:(NSString *)string;
+
+@end
diff --git a/Firebase/Database/FIndex.m b/Firebase/Database/FIndex.m
new file mode 100644
index 0000000..61980c7
--- /dev/null
+++ b/Firebase/Database/FIndex.m
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIndex.h"
+
+#import "FKeyIndex.h"
+#import "FValueIndex.h"
+#import "FPathIndex.h"
+#import "FPriorityIndex.h"
+
+@implementation FIndex
+
++ (id<FIndex>)indexFromQueryDefinition:(NSString *)string {
+ if ([string isEqualToString:@".key"]) {
+ return [FKeyIndex keyIndex];
+ } else if ([string isEqualToString:@".value"]) {
+ return [FValueIndex valueIndex];
+ } else if ([string isEqualToString:@".priority"]) {
+ return [FPriorityIndex priorityIndex];
+ } else {
+ return [[FPathIndex alloc] initWithPath:[[FPath alloc] initWith:string]];
+ }
+}
+
+@end
diff --git a/Firebase/Database/FKeyIndex.h b/Firebase/Database/FKeyIndex.h
new file mode 100644
index 0000000..a6bf787
--- /dev/null
+++ b/Firebase/Database/FKeyIndex.h
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+#import "FIndex.h"
+
+
+@interface FKeyIndex : NSObject<FIndex>
++ (id<FIndex>) keyIndex;
+@end
diff --git a/Firebase/Database/FKeyIndex.m b/Firebase/Database/FKeyIndex.m
new file mode 100644
index 0000000..68ad461
--- /dev/null
+++ b/Firebase/Database/FKeyIndex.m
@@ -0,0 +1,115 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FKeyIndex.h"
+#import "FNamedNode.h"
+#import "FSnapshotUtilities.h"
+#import "FUtilities.h"
+#import "FEmptyNode.h"
+
+@interface FKeyIndex ()
+
+@property (nonatomic, strong) FNamedNode *maxPost;
+
+@end
+
+@implementation FKeyIndex
+
+- (id)init {
+ self = [super init];
+ if (self) {
+ self.maxPost = [[FNamedNode alloc] initWithName:[FUtilities maxName] andNode:[FEmptyNode emptyNode]];
+ }
+ return self;
+
+}
+
+- (NSComparisonResult) compareKey:(NSString *)key1
+ andNode:(id<FNode>)node1
+ toOtherKey:(NSString *)key2
+ andNode:(id<FNode>)node2
+{
+ return [FUtilities compareKey:key1 toKey:key2];
+}
+
+- (NSComparisonResult) compareKey:(NSString *)key1
+ andNode:(id<FNode>)node1
+ toOtherKey:(NSString *)key2
+ andNode:(id<FNode>)node2
+ reverse:(BOOL)reverse
+{
+ if (reverse) {
+ return [self compareKey:key2 andNode:node2 toOtherKey:key1 andNode:node1];
+ } else {
+ return [self compareKey:key1 andNode:node1 toOtherKey:key2 andNode:node2];
+ }
+}
+
+- (NSComparisonResult) compareNamedNode:(FNamedNode *)namedNode1 toNamedNode:(FNamedNode *)namedNode2
+{
+ return [self compareKey:namedNode1.name andNode:namedNode1.node toOtherKey:namedNode2.name andNode:namedNode2.node];
+}
+
+- (BOOL)isDefinedOn:(id <FNode>)node {
+ return YES;
+}
+
+- (BOOL)indexedValueChangedBetween:(id <FNode>)oldNode and:(id <FNode>)newNode {
+ return NO; // The key for a node never changes.
+}
+
+- (FNamedNode *)minPost {
+ return [FNamedNode min];
+}
+
+- (FNamedNode *)makePost:(id<FNode>)indexValue name:(NSString*)name {
+ NSString *key = indexValue.val;
+ NSAssert([key isKindOfClass:[NSString class]], @"KeyIndex indexValue must always be a string.");
+ // We just use empty node, but it'll never be compared, since our comparator only looks at name.
+ return [[FNamedNode alloc] initWithName:key andNode:[FEmptyNode emptyNode]];
+}
+
+- (NSString *) queryDefinition {
+ return @".key";
+}
+
+- (NSString *) description {
+ return @"FKeyIndex";
+}
+
+- (id)copyWithZone:(NSZone *)zone {
+ return self;
+}
+
+- (BOOL) isEqual:(id)other {
+ // since we're a singleton.
+ return (other == self);
+}
+
+- (NSUInteger) hash {
+ return [@".key" hash];
+}
+
+
++ (id<FIndex>) keyIndex {
+ static id<FIndex> keyIndex;
+ static dispatch_once_t once;
+ dispatch_once(&once, ^{
+ keyIndex = [[FKeyIndex alloc] init];
+ });
+ return keyIndex;
+}
+@end
diff --git a/Firebase/Database/FListenComplete.h b/Firebase/Database/FListenComplete.h
new file mode 100644
index 0000000..914a3e4
--- /dev/null
+++ b/Firebase/Database/FListenComplete.h
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+#import "FOperation.h"
+
+
+@interface FListenComplete : NSObject <FOperation>
+
+- (id) initWithSource:(FOperationSource *)aSource path:(FPath *)aPath;
+
+@property (nonatomic, strong, readonly) FOperationSource *source;
+@property (nonatomic, strong, readonly) FPath *path;
+@property (nonatomic, readonly) FOperationType type;
+
+@end
diff --git a/Firebase/Database/FListenComplete.m b/Firebase/Database/FListenComplete.m
new file mode 100644
index 0000000..8573075
--- /dev/null
+++ b/Firebase/Database/FListenComplete.m
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FListenComplete.h"
+#import "FOperationSource.h"
+#import "FPath.h"
+
+@interface FListenComplete ()
+@property (nonatomic, strong, readwrite) FOperationSource *source;
+@property (nonatomic, strong, readwrite) FPath *path;
+@property (nonatomic, readwrite) FOperationType type;
+@end
+
+@implementation FListenComplete
+- (id) initWithSource:(FOperationSource *)aSource path:(FPath *)aPath {
+ NSAssert(!aSource.fromUser, @"Can't have a listen complete from a user source");
+ self = [super init];
+ if (self) {
+ self.source = aSource;
+ self.path = aPath;
+ self.type = FOperationTypeListenComplete;
+ }
+ return self;
+}
+
+- (id <FOperation>) operationForChild:(NSString *)childKey {
+ if ([self.path isEmpty]) {
+ return [[FListenComplete alloc] initWithSource:self.source path:[FPath empty]];
+ } else {
+ return [[FListenComplete alloc] initWithSource:self.source path:[self.path popFront]];
+ }
+}
+
+- (NSString *) description {
+ return [NSString stringWithFormat:@"FListenComplete { path=%@, source=%@ }", self.path, self.source];
+}
+
+@end
diff --git a/Firebase/Database/FMaxNode.h b/Firebase/Database/FMaxNode.h
new file mode 100644
index 0000000..6aff8c6
--- /dev/null
+++ b/Firebase/Database/FMaxNode.h
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+#import "FChildrenNode.h"
+
+
+@interface FMaxNode : FChildrenNode
+ + (id<FNode>) maxNode;
+@end
diff --git a/Firebase/Database/FMaxNode.m b/Firebase/Database/FMaxNode.m
new file mode 100644
index 0000000..3c93684
--- /dev/null
+++ b/Firebase/Database/FMaxNode.m
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FMaxNode.h"
+#import "FUtilities.h"
+#import "FEmptyNode.h"
+
+
+@implementation FMaxNode {
+
+}
+- (id) init {
+ self = [super init];
+ if (self) {
+
+ }
+ return self;
+}
+
++ (id<FNode>) maxNode {
+ static FMaxNode *maxNode = nil;
+ static dispatch_once_t once;
+ dispatch_once(&once, ^{
+ maxNode = [[FMaxNode alloc] init];
+ });
+ return maxNode;
+}
+
+- (NSComparisonResult) compare:(id<FNode>)other {
+ if (other == self) {
+ return NSOrderedSame;
+ } else {
+ return NSOrderedDescending;
+ }
+}
+
+- (BOOL)isEqual:(id)other {
+ return other == self;
+}
+
+- (id<FNode>) getImmediateChild:(NSString *) childName {
+ return [FEmptyNode emptyNode];
+}
+
+- (BOOL) isEmpty {
+ return NO;
+}
+@end
diff --git a/Firebase/Database/FNamedNode.h b/Firebase/Database/FNamedNode.h
new file mode 100644
index 0000000..ac9baa6
--- /dev/null
+++ b/Firebase/Database/FNamedNode.h
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+#import "FNode.h"
+
+@interface FNamedNode : NSObject<NSCopying>
+
+@property (nonatomic, strong, readonly) NSString* name;
+@property (nonatomic, strong, readonly) id<FNode> node;
+
+
+-(id)initWithName:(NSString*)name andNode:(id<FNode>)node;
+
++ (FNamedNode *)nodeWithName:(NSString *)name node:(id<FNode>)node;
+
++ (FNamedNode*) min;
++ (FNamedNode*) max;
+@end
diff --git a/Firebase/Database/FNamedNode.m b/Firebase/Database/FNamedNode.m
new file mode 100644
index 0000000..d11787b
--- /dev/null
+++ b/Firebase/Database/FNamedNode.m
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FNamedNode.h"
+#import "FUtilities.h"
+#import "FEmptyNode.h"
+#import "FMaxNode.h"
+#import "FIndex.h"
+
+@interface FNamedNode ()
+@property (nonatomic, strong, readwrite) NSString* name;
+@property (nonatomic, strong, readwrite) id<FNode> node;
+@end
+
+@implementation FNamedNode
+
++ (FNamedNode *)nodeWithName:(NSString *)name node:(id<FNode>)node
+{
+ return [[FNamedNode alloc] initWithName:name andNode:node];
+}
+
+- (id)initWithName:(NSString *)name andNode:(id <FNode>)node {
+ self = [super init];
+ if (self) {
+ self.name = name;
+ self.node = node;
+ }
+ return self;
+}
+
+- (id)copy
+{
+ return self;
+}
+
+- (id)copyWithZone:(NSZone *)zone
+{
+ return self;
+}
+
++ (FNamedNode *)min {
+ static FNamedNode *min = nil;
+ static dispatch_once_t once;
+ dispatch_once(&once, ^{
+ min = [[FNamedNode alloc] initWithName:[FUtilities minName] andNode:[FEmptyNode emptyNode]];
+ });
+ return min;
+}
+
++ (FNamedNode *)max {
+ static FNamedNode *max = nil;
+ static dispatch_once_t once;
+ dispatch_once(&once, ^{
+ max = [[FNamedNode alloc] initWithName:[FUtilities maxName] andNode:[FMaxNode maxNode]];
+ });
+ return max;
+}
+
+- (NSString *) description {
+ return [NSString stringWithFormat:@"NamedNode[%@] %@", self.name, self.node];
+}
+
+- (BOOL) isEqual:(id)object {
+ if (self == object) { return YES; }
+ if (object == nil || ![object isKindOfClass:[FNamedNode class]]) { return NO; }
+
+ FNamedNode *namedNode = object;
+ if (![self.name isEqualToString:namedNode.name]) { return NO; }
+ if (![self.node isEqual:namedNode.node]) { return NO; }
+
+ return YES;
+}
+
+- (NSUInteger) hash {
+ NSUInteger nameHash = [self.name hash];
+ NSUInteger nodeHash = [self.node hash];
+ NSUInteger result = 31 * nameHash + nodeHash;
+ return result;
+}
+
+@end
diff --git a/Firebase/Database/FPathIndex.h b/Firebase/Database/FPathIndex.h
new file mode 100644
index 0000000..cf92ad1
--- /dev/null
+++ b/Firebase/Database/FPathIndex.h
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+#import "FIndex.h"
+#import "FPath.h"
+
+@interface FPathIndex : NSObject<FIndex>
+- (id) initWithPath:(FPath *)path;
+@end
diff --git a/Firebase/Database/FPathIndex.m b/Firebase/Database/FPathIndex.m
new file mode 100644
index 0000000..39913aa
--- /dev/null
+++ b/Firebase/Database/FPathIndex.m
@@ -0,0 +1,125 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FPathIndex.h"
+#import "FUtilities.h"
+#import "FMaxNode.h"
+#import "FEmptyNode.h"
+#import "FSnapshotUtilities.h"
+#import "FNamedNode.h"
+#import "FPath.h"
+
+@interface FPathIndex ()
+ @property (nonatomic, strong) FPath *path;
+@end
+
+@implementation FPathIndex
+
+- (id) initWithPath:(FPath *)path {
+ self = [super init];
+ if (self) {
+ if (path.isEmpty || [path.getFront isEqualToString:@".priority"]) {
+ [NSException raise:NSInvalidArgumentException format:@"Invalid path for PathIndex: %@", path];
+ }
+ _path = path;
+ }
+ return self;
+}
+
+- (NSComparisonResult) compareKey:(NSString *)key1
+ andNode:(id<FNode>)node1
+ toOtherKey:(NSString *)key2
+ andNode:(id<FNode>)node2
+{
+ id<FNode> child1 = [node1 getChild:self.path];
+ id<FNode> child2 = [node2 getChild:self.path];
+ NSComparisonResult indexCmp = [child1 compare:child2];
+ if (indexCmp == NSOrderedSame) {
+ return [FUtilities compareKey:key1 toKey:key2];
+ } else {
+ return indexCmp;
+ }
+}
+
+- (NSComparisonResult) compareKey:(NSString *)key1
+ andNode:(id<FNode>)node1
+ toOtherKey:(NSString *)key2
+ andNode:(id<FNode>)node2
+ reverse:(BOOL)reverse
+{
+ if (reverse) {
+ return [self compareKey:key2 andNode:node2 toOtherKey:key1 andNode:node1];
+ } else {
+ return [self compareKey:key1 andNode:node1 toOtherKey:key2 andNode:node2];
+ }
+}
+
+- (NSComparisonResult) compareNamedNode:(FNamedNode *)namedNode1 toNamedNode:(FNamedNode *)namedNode2
+{
+ return [self compareKey:namedNode1.name andNode:namedNode1.node toOtherKey:namedNode2.name andNode:namedNode2.node];
+}
+
+- (BOOL)isDefinedOn:(id <FNode>)node {
+ return ![node getChild:self.path].isEmpty;
+}
+
+- (BOOL)indexedValueChangedBetween:(id <FNode>)oldNode and:(id <FNode>)newNode {
+ id<FNode> oldValue = [oldNode getChild:self.path];
+ id<FNode> newValue = [newNode getChild:self.path];
+ return [oldValue compare:newValue] != NSOrderedSame;
+}
+
+- (FNamedNode *)minPost {
+ return FNamedNode.min;
+}
+
+- (FNamedNode *)maxPost {
+ id<FNode> maxNode = [[FEmptyNode emptyNode] updateChild:self.path
+ withNewChild:[FMaxNode maxNode]];
+
+ return [[FNamedNode alloc] initWithName:[FUtilities maxName] andNode:maxNode];
+}
+
+- (FNamedNode*)makePost:(id<FNode>)indexValue name:(NSString*)name {
+ id<FNode> node = [[FEmptyNode emptyNode] updateChild:self.path withNewChild:indexValue];
+ return [[FNamedNode alloc] initWithName:name andNode:node];
+}
+
+- (NSString *)queryDefinition {
+ return [self.path wireFormat];
+}
+
+- (NSString *)description {
+ return [NSString stringWithFormat:@"FPathIndex(%@)", self.path];
+}
+
+- (id)copyWithZone:(NSZone *)zone {
+ // Safe since we're immutable.
+ return self;
+}
+
+- (BOOL) isEqual:(id)other {
+ if (![other isKindOfClass:[FPathIndex class]]) {
+ return NO;
+ }
+ return ([self.path isEqual:((FPathIndex*)other).path]);
+}
+
+- (NSUInteger) hash {
+ return [self.path hash];
+}
+
+@end
diff --git a/Firebase/Database/FPriorityIndex.h b/Firebase/Database/FPriorityIndex.h
new file mode 100644
index 0000000..8b5904d
--- /dev/null
+++ b/Firebase/Database/FPriorityIndex.h
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+#import "FIndex.h"
+
+@interface FPriorityIndex : NSObject<FIndex>
++ (id<FIndex>) priorityIndex;
+@end
diff --git a/Firebase/Database/FPriorityIndex.m b/Firebase/Database/FPriorityIndex.m
new file mode 100644
index 0000000..2d06ffa
--- /dev/null
+++ b/Firebase/Database/FPriorityIndex.m
@@ -0,0 +1,118 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FPriorityIndex.h"
+
+#import "FNode.h"
+#import "FUtilities.h"
+#import "FNamedNode.h"
+#import "FEmptyNode.h"
+#import "FLeafNode.h"
+#import "FMaxNode.h"
+
+// TODO: Abstract into some common base class?
+
+@implementation FPriorityIndex
+
+- (NSComparisonResult) compareKey:(NSString *)key1
+ andNode:(id<FNode>)node1
+ toOtherKey:(NSString *)key2
+ andNode:(id<FNode>)node2
+{
+ id<FNode> child1 = [node1 getPriority];
+ id<FNode> child2 = [node2 getPriority];
+ NSComparisonResult indexCmp = [child1 compare:child2];
+ if (indexCmp == NSOrderedSame) {
+ return [FUtilities compareKey:key1 toKey:key2];
+ } else {
+ return indexCmp;
+ }
+}
+
+- (NSComparisonResult) compareKey:(NSString *)key1
+ andNode:(id<FNode>)node1
+ toOtherKey:(NSString *)key2
+ andNode:(id<FNode>)node2
+ reverse:(BOOL)reverse
+{
+ if (reverse) {
+ return [self compareKey:key2 andNode:node2 toOtherKey:key1 andNode:node1];
+ } else {
+ return [self compareKey:key1 andNode:node1 toOtherKey:key2 andNode:node2];
+ }
+}
+
+- (NSComparisonResult) compareNamedNode:(FNamedNode *)namedNode1 toNamedNode:(FNamedNode *)namedNode2
+{
+ return [self compareKey:namedNode1.name andNode:namedNode1.node toOtherKey:namedNode2.name andNode:namedNode2.node];
+}
+
+- (BOOL)isDefinedOn:(id <FNode>)node {
+ return !node.getPriority.isEmpty;
+}
+
+- (BOOL)indexedValueChangedBetween:(id <FNode>)oldNode and:(id <FNode>)newNode {
+ id<FNode> oldValue = [oldNode getPriority];
+ id<FNode> newValue = [newNode getPriority];
+ return ![oldValue isEqual:newValue];
+}
+
+- (FNamedNode *)minPost {
+ return FNamedNode.min;
+}
+
+- (FNamedNode *)maxPost {
+ return [self makePost:[FMaxNode maxNode] name:[FUtilities maxName]];
+}
+
+- (FNamedNode*)makePost:(id<FNode>)indexValue name:(NSString*)name {
+ id<FNode> node = [[FLeafNode alloc] initWithValue:@"[PRIORITY-POST]" withPriority:indexValue];
+ return [[FNamedNode alloc] initWithName:name andNode:node];
+}
+
+- (NSString *)queryDefinition {
+ return @".priority";
+}
+
+- (NSString *)description {
+ return @"FPriorityIndex";
+}
+
+- (id)copyWithZone:(NSZone *)zone {
+ // Safe since we're immutable.
+ return self;
+}
+
+- (BOOL) isEqual:(id)other {
+ return [other isKindOfClass:[FPriorityIndex class]];
+}
+
+- (NSUInteger) hash {
+ // chosen by a fair dice roll. Guaranteed to be random
+ return 3155577;
+}
+
++ (id<FIndex>) priorityIndex {
+ static id<FIndex> index;
+ static dispatch_once_t once;
+ dispatch_once(&once, ^{
+ index = [[FPriorityIndex alloc] init];
+ });
+
+ return index;
+}
+
+@end
diff --git a/Firebase/Database/FRangedFilter.h b/Firebase/Database/FRangedFilter.h
new file mode 100644
index 0000000..1457778
--- /dev/null
+++ b/Firebase/Database/FRangedFilter.h
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+#import "FNodeFilter.h"
+
+@class FQueryParams;
+@class FNamedNode;
+
+@interface FRangedFilter : NSObject<FNodeFilter>
+
+- (id) initWithQueryParams:(FQueryParams *)params;
+- (BOOL) matchesKey:(NSString *)key andNode:(id<FNode>)node;
+
+
+@property (nonatomic, strong, readonly) FNamedNode *startPost;
+@property (nonatomic, strong, readonly) FNamedNode *endPost;
+
+@end
diff --git a/Firebase/Database/FRangedFilter.m b/Firebase/Database/FRangedFilter.m
new file mode 100644
index 0000000..5c4bbeb
--- /dev/null
+++ b/Firebase/Database/FRangedFilter.m
@@ -0,0 +1,118 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FRangedFilter.h"
+#import "FChildChangeAccumulator.h"
+#import "FNamedNode.h"
+#import "FQueryParams.h"
+#import "FIndexedFilter.h"
+#import "FQueryParams.h"
+#import "FEmptyNode.h"
+#import "FChildrenNode.h"
+#import "FIndexedNode.h"
+
+@interface FRangedFilter ()
+@property (nonatomic, strong, readwrite) id<FNodeFilter> indexedFilter;
+@property (nonatomic, strong, readwrite) id<FIndex> index;
+@property (nonatomic, strong, readwrite) FNamedNode *startPost;
+@property (nonatomic, strong, readwrite) FNamedNode *endPost;
+@end
+
+@implementation FRangedFilter
+- (id) initWithQueryParams:(FQueryParams *)params {
+ self = [super init];
+ if (self) {
+ self.indexedFilter = [[FIndexedFilter alloc] initWithIndex:params.index];
+ self.index = params.index;
+ self.startPost = [FRangedFilter startPostFromQueryParams:params];
+ self.endPost = [FRangedFilter endPostFromQueryParams:params];
+ }
+ return self;
+}
+
+
++ (FNamedNode *) startPostFromQueryParams:(FQueryParams *)params {
+ if ([params hasStart]) {
+ NSString *startKey = params.indexStartKey;
+ return [params.index makePost:params.indexStartValue name:startKey];
+ } else {
+ return params.index.minPost;
+ }
+}
+
++ (FNamedNode *) endPostFromQueryParams:(FQueryParams *)params {
+ if ([params hasEnd]) {
+ NSString *endKey = params.indexEndKey;
+ return [params.index makePost:params.indexEndValue name:endKey];
+ } else {
+ return params.index.maxPost;
+ }
+}
+
+- (BOOL) matchesKey:(NSString *)key andNode:(id<FNode>)node {
+ return ([self.index compareKey:self.startPost.name andNode:self.startPost.node toOtherKey:key andNode:node] <= NSOrderedSame &&
+ [self.index compareKey:key andNode:node toOtherKey:self.endPost.name andNode:self.endPost.node] <= NSOrderedSame);
+}
+
+- (FIndexedNode *)updateChildIn:(FIndexedNode *)oldSnap
+ forChildKey:(NSString *)childKey
+ newChild:(id<FNode>)newChildSnap
+ affectedPath:(FPath *)affectedPath
+ fromSource:(id<FCompleteChildSource>)source
+ accumulator:(FChildChangeAccumulator *)optChangeAccumulator
+{
+ if (![self matchesKey:childKey andNode:newChildSnap]) {
+ newChildSnap = [FEmptyNode emptyNode];
+ }
+ return [self.indexedFilter updateChildIn:oldSnap
+ forChildKey:childKey
+ newChild:newChildSnap
+ affectedPath:affectedPath
+ fromSource:source
+ accumulator:optChangeAccumulator];
+}
+
+- (FIndexedNode *) updateFullNode:(FIndexedNode *)oldSnap
+ withNewNode:(FIndexedNode *)newSnap
+ accumulator:(FChildChangeAccumulator *)optChangeAccumulator
+{
+ __block FIndexedNode *filtered;
+ if (newSnap.node.isLeafNode) {
+ // Make sure we have a children node with the correct index, not a leaf node
+ filtered = [FIndexedNode indexedNodeWithNode:[FEmptyNode emptyNode] index:self.index];
+ } else {
+ // Dont' support priorities on queries
+ filtered = [newSnap updatePriority:[FEmptyNode emptyNode]];
+ [newSnap.node enumerateChildrenUsingBlock:^(NSString *key, id<FNode> node, BOOL *stop) {
+ if (![self matchesKey:key andNode:node]) {
+ filtered = [filtered updateChild:key withNewChild:[FEmptyNode emptyNode]];
+ }
+ }];
+ }
+ return [self.indexedFilter updateFullNode:oldSnap withNewNode:filtered accumulator:optChangeAccumulator];
+}
+
+- (FIndexedNode *) updatePriority:(id<FNode>)priority forNode:(FIndexedNode *)oldSnap
+{
+ // Don't support priorities on queries
+ return oldSnap;
+}
+
+- (BOOL) filtersNodes {
+ return YES;
+}
+
+@end
diff --git a/Firebase/Database/FTransformedEnumerator.h b/Firebase/Database/FTransformedEnumerator.h
new file mode 100644
index 0000000..75391a8
--- /dev/null
+++ b/Firebase/Database/FTransformedEnumerator.h
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+
+@interface FTransformedEnumerator : NSEnumerator
+- (id)initWithEnumerator:(NSEnumerator*) enumerator andTransform:(id (^)(id))transform;
+- (id)nextObject;
+
+@end
diff --git a/Firebase/Database/FTransformedEnumerator.m b/Firebase/Database/FTransformedEnumerator.m
new file mode 100644
index 0000000..bb36e94
--- /dev/null
+++ b/Firebase/Database/FTransformedEnumerator.m
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FTransformedEnumerator.h"
+
+@interface FTransformedEnumerator ()
+@property (nonatomic, strong) NSEnumerator *enumerator;
+@property (nonatomic, copy) id (^transform)(id);
+@end
+
+@implementation FTransformedEnumerator
+- (id)initWithEnumerator:(NSEnumerator *)enumerator andTransform:(id (^)(id))transform {
+ self = [super init];
+ if (self) {
+ self.enumerator = enumerator;
+ self.transform = transform;
+ }
+ return self;
+}
+
+- (id)nextObject {
+ id next = self.enumerator.nextObject;
+ if (next != nil) {
+ return self.transform(next);
+ } else {
+ return nil;
+ }
+}
+
+@end
diff --git a/Firebase/Database/FTreeSortedDictionary.h b/Firebase/Database/FTreeSortedDictionary.h
new file mode 100644
index 0000000..de75988
--- /dev/null
+++ b/Firebase/Database/FTreeSortedDictionary.h
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * @fileoverview Implementation of an immutable SortedMap using a Left-leaning
+ * Red-Black Tree, adapted from the implementation in Mugs
+ * (http://mads379.github.com/mugs/) by Mads Hartmann Jensen
+ * (mads379@gmail.com).
+ *
+ * Original paper on Left-leaning Red-Black Trees:
+ * http://www.cs.princeton.edu/~rs/talks/LLRB/LLRB.pdf
+ *
+ * Invariant 1: No red node has a red child
+ * Invariant 2: Every leaf path has the same number of black nodes
+ * Invariant 3: Only the left child can be red (left leaning)
+ */
+
+#import <Foundation/Foundation.h>
+#import "FImmutableSortedDictionary.h"
+#import "FLLRBNode.h"
+
+@interface FTreeSortedDictionary : FImmutableSortedDictionary
+
+@property (nonatomic, copy, readonly) NSComparator comparator;
+@property (nonatomic, strong, readonly) id<FLLRBNode> root;
+
+- (id)initWithComparator:(NSComparator)aComparator;
+
+// Override methods to return subtype
+- (FTreeSortedDictionary *) insertKey:(id)aKey withValue:(id)aValue;
+- (FTreeSortedDictionary *) removeKey:(id)aKey;
+
+@end
diff --git a/Firebase/Database/FTreeSortedDictionary.m b/Firebase/Database/FTreeSortedDictionary.m
new file mode 100644
index 0000000..d3b00f9
--- /dev/null
+++ b/Firebase/Database/FTreeSortedDictionary.m
@@ -0,0 +1,342 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FTreeSortedDictionary.h"
+#import "FLLRBEmptyNode.h"
+#import "FLLRBValueNode.h"
+#import "FTreeSortedDictionaryEnumerator.h"
+
+typedef void (^fbt_void_nsnumber_int)(NSNumber* color, NSUInteger chunkSize);
+
+@interface FTreeSortedDictionary ()
+
+@property (nonatomic, strong) id<FLLRBNode> root;
+@property (nonatomic, copy, readwrite) NSComparator comparator;
+
+@end
+
+@implementation FTreeSortedDictionary
+
+- (id)initWithComparator:(NSComparator)aComparator {
+ self = [super init];
+ if (self) {
+ self.root = [FLLRBEmptyNode emptyNode];
+ self.comparator = aComparator;
+ }
+ return self;
+}
+
+- (id)initWithComparator:(NSComparator)aComparator withRoot:(__unsafe_unretained id<FLLRBNode>)aRoot {
+ self = [super init];
+ if (self) {
+ self.root = aRoot;
+ self.comparator = aComparator;
+ }
+ return self;
+}
+
+/**
+ * Returns a copy of the map, with the specified key/value added or replaced.
+ */
+- (FTreeSortedDictionary *) insertKey:(__unsafe_unretained id)aKey withValue:(__unsafe_unretained id)aValue {
+ return [[FTreeSortedDictionary alloc] initWithComparator:self.comparator
+ withRoot:[[self.root insertKey:aKey forValue:aValue withComparator:self.comparator]
+ copyWith:nil
+ withValue:nil
+ withColor:BLACK
+ withLeft:nil
+ withRight:nil]];
+}
+
+
+- (FTreeSortedDictionary *) removeKey:(__unsafe_unretained id)aKey {
+ // Remove is somewhat expensive even if the key doesn't exist (the tree does rebalancing and stuff). So avoid it.
+ if (![self contains:aKey]) {
+ return self;
+ } else {
+ return [[FTreeSortedDictionary alloc]
+ initWithComparator:self.comparator
+ withRoot:[[self.root remove:aKey withComparator:self.comparator]
+ copyWith:nil
+ withValue:nil
+ withColor:BLACK
+ withLeft:nil
+ withRight:nil]];
+ }
+}
+
+- (id) get:(__unsafe_unretained id) key {
+ if (key == nil) {
+ return nil;
+ }
+ NSComparisonResult cmp;
+ id<FLLRBNode> node = self.root;
+ while(![node isEmpty]) {
+ cmp = self.comparator(key, node.key);
+ if(cmp == NSOrderedSame) {
+ return node.value;
+ }
+ else if (cmp == NSOrderedAscending) {
+ node = node.left;
+ }
+ else {
+ node = node.right;
+ }
+ }
+ return nil;
+}
+
+- (id) getPredecessorKey:(__unsafe_unretained id) key {
+ NSComparisonResult cmp;
+ id<FLLRBNode> node = self.root;
+ id<FLLRBNode> rightParent = nil;
+ while(![node isEmpty]) {
+ cmp = self.comparator(key, node.key);
+ if(cmp == NSOrderedSame) {
+ if(![node.left isEmpty]) {
+ node = node.left;
+ while(! [node.right isEmpty]) {
+ node = node.right;
+ }
+ return node.key;
+ }
+ else if (rightParent != nil) {
+ return rightParent.key;
+ }
+ else {
+ return nil;
+ }
+ }
+ else if (cmp == NSOrderedAscending) {
+ node = node.left;
+ }
+ else if (cmp == NSOrderedDescending) {
+ rightParent = node;
+ node = node.right;
+ }
+ }
+ @throw [NSException exceptionWithName:@"NonexistentKey" reason:@"getPredecessorKey called with nonexistent key." userInfo:@{@"key": [key description] }];
+}
+
+- (BOOL) isEmpty {
+ return [self.root isEmpty];
+}
+
+- (int) count {
+ return [self.root count];
+}
+
+- (id) minKey {
+ return [self.root minKey];
+}
+
+- (id) maxKey {
+ return [self.root maxKey];
+}
+
+- (void) enumerateKeysAndObjectsUsingBlock:(void (^)(id, id, BOOL *))block
+{
+ [self enumerateKeysAndObjectsReverse:NO usingBlock:block];
+}
+
+- (void) enumerateKeysAndObjectsReverse:(BOOL)reverse usingBlock:(void (^)(id, id, BOOL *))block
+{
+ if (reverse) {
+ __block BOOL stop = NO;
+ [self.root reverseTraversal:^BOOL(id key, id value) {
+ block(key, value, &stop);
+ return stop;
+ }];
+ } else {
+ __block BOOL stop = NO;
+ [self.root inorderTraversal:^BOOL(id key, id value) {
+ block(key, value, &stop);
+ return stop;
+ }];
+ }
+}
+
+- (BOOL) contains:(__unsafe_unretained id)key {
+ return ([self objectForKey:key] != nil);
+}
+
+- (NSEnumerator *) keyEnumerator {
+ return [[FTreeSortedDictionaryEnumerator alloc]
+ initWithImmutableSortedDictionary:self startKey:nil isReverse:NO];
+}
+
+- (NSEnumerator *) keyEnumeratorFrom:(id)startKey {
+ return [[FTreeSortedDictionaryEnumerator alloc]
+ initWithImmutableSortedDictionary:self startKey:startKey isReverse:NO];
+}
+
+- (NSEnumerator *) reverseKeyEnumerator {
+ return [[FTreeSortedDictionaryEnumerator alloc]
+ initWithImmutableSortedDictionary:self startKey:nil isReverse:YES];
+}
+
+- (NSEnumerator *) reverseKeyEnumeratorFrom:(id)startKey {
+ return [[FTreeSortedDictionaryEnumerator alloc]
+ initWithImmutableSortedDictionary:self startKey:startKey isReverse:YES];
+}
+
+
+#pragma mark -
+#pragma mark Tree Builder
+
+// Code to efficiently build a RB Tree
+typedef struct _base1_2list {
+ unsigned int bits;
+ unsigned short count;
+ unsigned short current;
+} Base1_2List;
+
+Base1_2List *base1_2List_new(unsigned int length);
+void base1_2List_free(Base1_2List* list);
+unsigned int log_base2(unsigned int num);
+BOOL base1_2List_next(Base1_2List* list);
+
+unsigned int log_base2(unsigned int num) {
+ return (unsigned int)(log(num) / log(2));
+}
+
+/**
+ * Works like an iterator, so it moves to the next bit. Do not call more than list->count times.
+ * @return whether or not the next bit is a 1 in base {1,2}.
+ */
+BOOL base1_2List_next(Base1_2List* list) {
+ BOOL result = !(list->bits & (0x1 << list->current));
+ list->current--;
+ return result;
+}
+
+static inline unsigned bit_mask(int x) {
+ return (x >= sizeof(unsigned) * CHAR_BIT) ? (unsigned) -1 : (1U << x) - 1;
+}
+
+/**
+ * We represent the base{1,2} number as the combination of a binary number and a number of bits that we care about
+ * We iterate backwards, from most significant bit to least, to build up the llrb nodes. 0 base 2 => 1 base {1,2}, 1 base 2 => 2 base {1,2}
+ */
+Base1_2List *base1_2List_new(unsigned int length) {
+ size_t sz = sizeof(Base1_2List);
+ Base1_2List* list = calloc(1, sz);
+ // Calculate the number of bits that we care about
+ list->count = (unsigned short)log_base2(length + 1);
+ unsigned int mask = bit_mask(list->count);
+ list->bits = (length + 1) & mask;
+ list->current = list->count - 1;
+ return list;
+}
+
+
+void base1_2List_free(Base1_2List* list) {
+ free(list);
+}
+
++ (id<FLLRBNode>) buildBalancedTree:(NSArray *)keys dictionary:(NSDictionary *)dictionary subArrayStartIndex:(NSUInteger)startIndex length:(NSUInteger)length {
+ length = MIN(keys.count - startIndex, length); // Bound length by the actual length of the array
+ if (length == 0) {
+ return nil;
+ } else if (length == 1) {
+ id key = keys[startIndex];
+ return [[FLLRBValueNode alloc] initWithKey:key withValue:dictionary[key] withColor:BLACK withLeft:nil withRight:nil];
+ } else {
+ NSUInteger middle = length / 2;
+ id<FLLRBNode> left = [FTreeSortedDictionary buildBalancedTree:keys dictionary:dictionary subArrayStartIndex:startIndex length:middle];
+ id<FLLRBNode> right = [FTreeSortedDictionary buildBalancedTree:keys dictionary:dictionary subArrayStartIndex:(startIndex+middle+1) length:middle];
+ id key = keys[startIndex + middle];
+ return [[FLLRBValueNode alloc] initWithKey:key withValue:dictionary[key] withColor:BLACK withLeft:left withRight:right];
+ }
+}
+
++ (id<FLLRBNode>) rootFrom12List:(Base1_2List *)base1_2List keyList:(NSArray *)keyList dictionary:(NSDictionary *)dictionary {
+ __block id<FLLRBNode> root = nil;
+ __block id<FLLRBNode> node = nil;
+ __block NSUInteger index = keyList.count;
+
+ fbt_void_nsnumber_int buildPennant = ^(NSNumber* color, NSUInteger chunkSize) {
+ NSUInteger startIndex = index - chunkSize + 1;
+ index -= chunkSize;
+ id key = keyList[index];
+ id<FLLRBNode> childTree = [self buildBalancedTree:keyList dictionary:dictionary subArrayStartIndex:startIndex length:(chunkSize - 1)];
+ id<FLLRBNode> pennant = [[FLLRBValueNode alloc] initWithKey:key withValue:dictionary[key] withColor:color withLeft:nil withRight:childTree];
+ //attachPennant(pennant);
+ if (node) {
+ node.left = pennant;
+ node = pennant;
+ } else {
+ root = pennant;
+ node = pennant;
+ }
+ };
+
+ for (int i = 0; i < base1_2List->count; ++i) {
+ BOOL isOne = base1_2List_next(base1_2List);
+ NSUInteger chunkSize = (NSUInteger)pow(2.0, base1_2List->count - (i + 1));
+ if (isOne) {
+ buildPennant(BLACK, chunkSize);
+ } else {
+ buildPennant(BLACK, chunkSize);
+ buildPennant(RED, chunkSize);
+ }
+ }
+ return root;
+}
+
+/**
+ * Uses the algorithm linked here:
+ * http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.46.1458
+ */
+
++ (FImmutableSortedDictionary *)fromDictionary:(NSDictionary *)dictionary withComparator:(NSComparator)comparator
+{
+ // Steps:
+ // 0. Sort the array
+ // 1. Calculate the 1-2 number
+ // 2. Build From 1-2 number
+ // 0. for each digit in 1-2 number
+ // 0. calculate chunk size
+ // 1. build 1 or 2 pennants of that size
+ // 2. attach pennants and update node pointer
+ // 1. return root
+ NSMutableArray *sortedKeyList = [NSMutableArray arrayWithCapacity:dictionary.count];
+ [dictionary enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
+ [sortedKeyList addObject:key];
+ }];
+ [sortedKeyList sortUsingComparator:comparator];
+
+ [sortedKeyList enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
+ if (idx > 0) {
+ if (comparator(sortedKeyList[idx - 1], obj) != NSOrderedAscending) {
+ [NSException raise:NSInvalidArgumentException format:@"Can't create FImmutableSortedDictionary with keys with same ordering!"];
+ }
+ }
+ }];
+
+ Base1_2List* list = base1_2List_new((unsigned int)sortedKeyList.count);
+ id<FLLRBNode> root = [self rootFrom12List:list keyList:sortedKeyList dictionary:dictionary];
+ base1_2List_free(list);
+
+ if (root != nil) {
+ return [[FTreeSortedDictionary alloc] initWithComparator:comparator withRoot:root];
+ } else {
+ return [[FTreeSortedDictionary alloc] initWithComparator:comparator];
+ }
+}
+
+@end
+
diff --git a/Firebase/Database/FValueIndex.h b/Firebase/Database/FValueIndex.h
new file mode 100644
index 0000000..0f1c7f7
--- /dev/null
+++ b/Firebase/Database/FValueIndex.h
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+#import "FIndex.h"
+
+
+@interface FValueIndex : NSObject<FIndex>
++ (id<FIndex>) valueIndex;
+@end
diff --git a/Firebase/Database/FValueIndex.m b/Firebase/Database/FValueIndex.m
new file mode 100644
index 0000000..7ef9bff
--- /dev/null
+++ b/Firebase/Database/FValueIndex.m
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FValueIndex.h"
+#import "FNamedNode.h"
+#import "FSnapshotUtilities.h"
+#import "FUtilities.h"
+#import "FMaxNode.h"
+
+@implementation FValueIndex
+
+- (NSComparisonResult) compareKey:(NSString *)key1
+ andNode:(id<FNode>)node1
+ toOtherKey:(NSString *)key2
+ andNode:(id<FNode>)node2
+{
+ NSComparisonResult indexCmp = [node1 compare:node2];
+ if (indexCmp == NSOrderedSame) {
+ return [FUtilities compareKey:key1 toKey:key2];
+ } else {
+ return indexCmp;
+ }
+}
+
+- (NSComparisonResult) compareKey:(NSString *)key1
+ andNode:(id<FNode>)node1
+ toOtherKey:(NSString *)key2
+ andNode:(id<FNode>)node2
+ reverse:(BOOL)reverse
+{
+ if (reverse) {
+ return [self compareKey:key2 andNode:node2 toOtherKey:key1 andNode:node1];
+ } else {
+ return [self compareKey:key1 andNode:node1 toOtherKey:key2 andNode:node2];
+ }
+}
+
+- (NSComparisonResult) compareNamedNode:(FNamedNode *)namedNode1 toNamedNode:(FNamedNode *)namedNode2
+{
+ return [self compareKey:namedNode1.name andNode:namedNode1.node toOtherKey:namedNode2.name andNode:namedNode2.node];
+}
+
+- (BOOL)isDefinedOn:(id<FNode>)node {
+ return YES;
+}
+
+- (BOOL)indexedValueChangedBetween:(id<FNode>)oldNode and:(id<FNode>)newNode {
+ return ![oldNode isEqual:newNode];
+}
+
+- (FNamedNode *)minPost {
+ return FNamedNode.min;
+}
+
+- (FNamedNode *)maxPost {
+ return FNamedNode.max;
+}
+
+- (FNamedNode *)makePost:(id<FNode>)indexValue name:(NSString*)name {
+ return [[FNamedNode alloc] initWithName:name andNode:indexValue];
+}
+
+- (NSString *)queryDefinition {
+ return @".value";
+}
+
+- (NSString *) description {
+ return @"FValueIndex";
+}
+
+- (id)copyWithZone:(NSZone *)zone {
+ return self;
+}
+
+- (BOOL) isEqual:(id)other {
+ // since we're a singleton.
+ return (other == self);
+}
+
+- (NSUInteger) hash {
+ return [@".value" hash];
+}
+
+
++ (id<FIndex>) valueIndex {
+ static id<FIndex> valueIndex;
+ static dispatch_once_t once;
+ dispatch_once(&once, ^{
+ valueIndex = [[FValueIndex alloc] init];
+ });
+ return valueIndex;
+}
+@end
diff --git a/Firebase/Database/FViewProcessor.h b/Firebase/Database/FViewProcessor.h
new file mode 100644
index 0000000..59bfd2d
--- /dev/null
+++ b/Firebase/Database/FViewProcessor.h
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+@class FViewCache;
+@class FViewProcessorResult;
+@class FChildChangeAccumulator;
+@protocol FNode;
+@class FWriteTreeRef;
+@class FPath;
+@protocol FOperation;
+@protocol FNodeFilter;
+
+
+@interface FViewProcessor : NSObject
+
+- (id)initWithFilter:(id<FNodeFilter>)nodeFilter;
+
+- (FViewProcessorResult *)applyOperationOn:(FViewCache *)oldViewCache operation:(id<FOperation>)operation writesCache:(FWriteTreeRef *)writesCache completeCache:(id <FNode>)optCompleteCache;
+- (FViewCache *) revertUserWriteOn:(FViewCache *)viewCache
+ path:(FPath *)path
+ writesCache:(FWriteTreeRef *)writesCache
+ completeCache:(id<FNode>)optCompleteCache
+ accumulator:(FChildChangeAccumulator *)accumulator;
+
+
+@end
diff --git a/Firebase/Database/FViewProcessor.m b/Firebase/Database/FViewProcessor.m
new file mode 100644
index 0000000..41ff91d
--- /dev/null
+++ b/Firebase/Database/FViewProcessor.m
@@ -0,0 +1,654 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FViewProcessor.h"
+#import "FCompleteChildSource.h"
+#import "FWriteTreeRef.h"
+#import "FViewCache.h"
+#import "FCacheNode.h"
+#import "FNode.h"
+#import "FOperation.h"
+#import "FOperationSource.h"
+#import "FChildChangeAccumulator.h"
+#import "FNodeFilter.h"
+#import "FOverwrite.h"
+#import "FMerge.h"
+#import "FAckUserWrite.h"
+#import "FViewProcessorResult.h"
+#import "FIRDataEventType.h"
+#import "FChange.h"
+#import "FEmptyNode.h"
+#import "FChildrenNode.h"
+#import "FPath.h"
+#import "FKeyIndex.h"
+#import "FCompoundWrite.h"
+#import "FImmutableTree.h"
+
+/**
+* An implementation of FCompleteChildSource that never returns any additional children
+*/
+@interface FNoCompleteChildSource: NSObject<FCompleteChildSource>
+@end
+
+@implementation FNoCompleteChildSource
++ (FNoCompleteChildSource *) instance {
+ static FNoCompleteChildSource *source = nil;
+ static dispatch_once_t once;
+ dispatch_once(&once, ^{
+ source = [[FNoCompleteChildSource alloc] init];
+ });
+ return source;
+}
+
+- (id<FNode>) completeChild:(NSString *)childKey {
+ return nil;
+}
+
+- (FNamedNode *) childByIndex:(id<FIndex>)index afterChild:(FNamedNode *)child isReverse:(BOOL)reverse {
+ return nil;
+}
+@end
+
+/**
+* An implementation of FCompleteChildSource that uses a FWriteTree in addition to any other server data or
+* old event caches available to calculate complete children.
+*/
+@interface FWriteTreeCompleteChildSource: NSObject<FCompleteChildSource>
+@property (nonatomic, strong) FWriteTreeRef *writes;
+@property (nonatomic, strong) FViewCache *viewCache;
+@property (nonatomic, strong) id<FNode> optCompleteServerCache;
+@end
+
+@implementation FWriteTreeCompleteChildSource
+- (id) initWithWrites:(FWriteTreeRef *)writes viewCache:(FViewCache *)viewCache serverCache:(id<FNode>)optCompleteServerCache {
+ self = [super init];
+ if (self) {
+ self.writes = writes;
+ self.viewCache = viewCache;
+ self.optCompleteServerCache = optCompleteServerCache;
+ }
+ return self;
+}
+
+- (id<FNode>) completeChild:(NSString *)childKey {
+ FCacheNode *node = self.viewCache.cachedEventSnap;
+ if ([node isCompleteForChild:childKey]) {
+ return [node.node getImmediateChild:childKey];
+ } else {
+ FCacheNode *serverNode;
+ if (self.optCompleteServerCache) {
+ // Since we're only ever getting child nodes, we can use the key index here
+ FIndexedNode *indexed = [FIndexedNode indexedNodeWithNode:self.optCompleteServerCache index:[FKeyIndex keyIndex]];
+ serverNode = [[FCacheNode alloc] initWithIndexedNode:indexed isFullyInitialized:YES isFiltered:NO];
+ } else {
+ serverNode = self.viewCache.cachedServerSnap;
+ }
+ return [self.writes calculateCompleteChild:childKey cache:serverNode];
+ }
+}
+
+- (FNamedNode *) childByIndex:(id<FIndex>)index afterChild:(FNamedNode *)child isReverse:(BOOL)reverse {
+ id<FNode> completeServerData = self.optCompleteServerCache != nil
+ ? self.optCompleteServerCache
+ : self.viewCache.completeServerSnap;
+ return [self.writes calculateNextNodeAfterPost:child
+ completeServerData:completeServerData
+ reverse:reverse
+ index:index];
+}
+
+@end
+
+@interface FViewProcessor ()
+@property (nonatomic, strong) id<FNodeFilter> filter;
+@end
+
+@implementation FViewProcessor
+
+- (id)initWithFilter:(id<FNodeFilter>)nodeFilter {
+ self = [super init];
+ if (self) {
+ self.filter = nodeFilter;
+ }
+ return self;
+}
+
+- (FViewProcessorResult *)applyOperationOn:(FViewCache *)oldViewCache operation:(id<FOperation>)operation writesCache:(FWriteTreeRef *)writesCache completeCache:(id <FNode>)optCompleteCache {
+ FChildChangeAccumulator *accumulator = [[FChildChangeAccumulator alloc] init];
+ FViewCache *newViewCache;
+
+ if (operation.type == FOperationTypeOverwrite) {
+ FOverwrite *overwrite = (FOverwrite *) operation;
+ if (operation.source.fromUser) {
+ newViewCache = [self applyUserOverwriteTo:oldViewCache
+ changePath:overwrite.path
+ changedSnap:overwrite.snap
+ writesCache:writesCache
+ completeCache:optCompleteCache
+ accumulator:accumulator];
+ } else {
+ NSAssert(operation.source.fromServer, @"Unknown source for overwrite.");
+ // We filter the node if it's a tagged update or the node has been previously filtered and the update is
+ // not at the root in which case it is ok (and necessary) to mark the node unfiltered again
+ BOOL filterServerNode = overwrite.source.isTagged || (oldViewCache.cachedServerSnap.isFiltered &&
+ !overwrite.path.isEmpty);
+ newViewCache = [self applyServerOverwriteTo:oldViewCache
+ changePath:overwrite.path
+ snap:overwrite.snap
+ writesCache:writesCache
+ completeCache:optCompleteCache
+ filterServerNode:filterServerNode
+ accumulator:accumulator];
+ }
+ } else if (operation.type == FOperationTypeMerge) {
+ FMerge *merge = (FMerge*)operation;
+ if (operation.source.fromUser) {
+ newViewCache = [self applyUserMergeTo:oldViewCache
+ path:merge.path
+ changedChildren:merge.children
+ writesCache:writesCache
+ completeCache:optCompleteCache
+ accumulator:accumulator];
+ } else {
+ NSAssert(operation.source.fromServer, @"Unknown source for merge.");
+ // We filter the node if it's a tagged update or the node has been previously filtered
+ BOOL filterServerNode = merge.source.isTagged || oldViewCache.cachedServerSnap.isFiltered;
+ newViewCache = [self applyServerMergeTo:oldViewCache
+ path:merge.path
+ changedChildren:merge.children
+ writesCache:writesCache
+ completeCache:optCompleteCache
+ filterServerNode:filterServerNode
+ accumulator:accumulator];
+ }
+ } else if (operation.type == FOperationTypeAckUserWrite) {
+ FAckUserWrite *ackWrite = (FAckUserWrite *) operation;
+ if (!ackWrite.revert) {
+ newViewCache = [self ackUserWriteOn:oldViewCache
+ ackPath:ackWrite.path
+ affectedTree:ackWrite.affectedTree
+ writesCache:writesCache
+ completeCache:optCompleteCache
+ accumulator:accumulator];
+ } else {
+ newViewCache = [self revertUserWriteOn:oldViewCache
+ path:ackWrite.path
+ writesCache:writesCache
+ completeCache:optCompleteCache
+ accumulator:accumulator];
+ }
+ } else if (operation.type == FOperationTypeListenComplete) {
+ newViewCache = [self listenCompleteOldCache:oldViewCache
+ path:operation.path
+ writesCache:writesCache
+ serverCache:optCompleteCache
+ accumulator:accumulator];
+ } else {
+ [NSException raise:NSInternalInconsistencyException format:@"Unknown operation encountered %zd.", operation.type];
+ return nil;
+ }
+
+ NSArray *changes = [self maybeAddValueFromOldViewCache:oldViewCache newViewCache:newViewCache changes:accumulator.changes];
+ FViewProcessorResult *results = [[FViewProcessorResult alloc] initWithViewCache:newViewCache changes:changes];
+ return results;
+}
+
+- (NSArray *) maybeAddValueFromOldViewCache:(FViewCache *)oldViewCache newViewCache:(FViewCache *)newViewCache changes:(NSArray *)changes {
+ NSArray *newChanges = changes;
+ FCacheNode *eventSnap = newViewCache.cachedEventSnap;
+ if (eventSnap.isFullyInitialized) {
+ BOOL isLeafOrEmpty = eventSnap.node.isLeafNode || eventSnap.node.isEmpty;
+ if ([changes count] > 0 ||
+ !oldViewCache.cachedEventSnap.isFullyInitialized ||
+ (isLeafOrEmpty && ![eventSnap.node isEqual:oldViewCache.completeEventSnap]) ||
+ ![eventSnap.node.getPriority isEqual:oldViewCache.completeEventSnap.getPriority]) {
+ FChange *valueChange = [[FChange alloc] initWithType:FIRDataEventTypeValue indexedNode:eventSnap.indexedNode];
+ NSMutableArray *mutableChanges = [changes mutableCopy];
+ [mutableChanges addObject:valueChange];
+ newChanges = mutableChanges;
+ }
+ }
+ return newChanges;
+}
+
+- (FViewCache *) generateEventCacheAfterServerEvent:(FViewCache *)viewCache
+ path:(FPath *)changePath
+ writesCache:(FWriteTreeRef *)writesCache
+ source:(id<FCompleteChildSource>)source
+ accumulator:(FChildChangeAccumulator *)accumulator {
+ FCacheNode *oldEventSnap = viewCache.cachedEventSnap;
+ if ([writesCache shadowingWriteAtPath:changePath] != nil) {
+ // we have a shadowing write, ignore changes.
+ return viewCache;
+ } else {
+ FIndexedNode *newEventCache;
+ if (changePath.isEmpty) {
+ // TODO: figure out how this plays with "sliding ack windows"
+ NSAssert(viewCache.cachedServerSnap.isFullyInitialized, @"If change path is empty, we must have complete server data");
+ id<FNode> nodeWithLocalWrites;
+ if (viewCache.cachedServerSnap.isFiltered) {
+ // We need to special case this, because we need to only apply writes to complete children, or
+ // we might end up raising events for incomplete children. If the server data is filtered deep
+ // writes cannot be guaranteed to be complete
+ id<FNode> serverCache = viewCache.completeServerSnap;
+ FChildrenNode *completeChildren = ([serverCache isKindOfClass:[FChildrenNode class]]) ? serverCache : [FEmptyNode emptyNode];
+ nodeWithLocalWrites = [writesCache calculateCompleteEventChildrenWithCompleteServerChildren:completeChildren];
+ } else {
+ nodeWithLocalWrites = [writesCache calculateCompleteEventCacheWithCompleteServerCache:viewCache.completeServerSnap];
+ }
+ FIndexedNode *indexedNode = [FIndexedNode indexedNodeWithNode:nodeWithLocalWrites index:self.filter.index];
+ newEventCache = [self.filter updateFullNode:viewCache.cachedEventSnap.indexedNode
+ withNewNode:indexedNode
+ accumulator:accumulator];
+ } else {
+ NSString *childKey = [changePath getFront];
+ if ([childKey isEqualToString:@".priority"]) {
+ NSAssert(changePath.length == 1, @"Can't have a priority with additional path components");
+ id<FNode> oldEventNode = oldEventSnap.node;
+ id<FNode> serverNode = viewCache.cachedServerSnap.node;
+ // we might have overwrites for this priority
+ id<FNode> updatedPriority = [writesCache calculateEventCacheAfterServerOverwriteWithChildPath:changePath
+ existingEventSnap:oldEventNode
+ existingServerSnap:serverNode];
+ if (updatedPriority != nil) {
+ newEventCache = [self.filter updatePriority:updatedPriority forNode:oldEventSnap.indexedNode];
+ } else {
+ // priority didn't change, keep old node
+ newEventCache = oldEventSnap.indexedNode;
+ }
+ } else {
+ FPath *childChangePath = [changePath popFront];
+ id<FNode> newEventChild;
+ if ([oldEventSnap isCompleteForChild:childKey]) {
+ id<FNode> serverNode = viewCache.cachedServerSnap.node;
+ id<FNode> eventChildUpdate = [writesCache calculateEventCacheAfterServerOverwriteWithChildPath:changePath existingEventSnap:oldEventSnap.node existingServerSnap:serverNode];
+ if (eventChildUpdate != nil) {
+ newEventChild = [[oldEventSnap.node getImmediateChild:childKey] updateChild:childChangePath withNewChild:eventChildUpdate];
+ } else {
+ // Nothing changed, just keep the old child
+ newEventChild = [oldEventSnap.node getImmediateChild:childKey];
+ }
+ } else {
+ newEventChild = [writesCache calculateCompleteChild:childKey cache:viewCache.cachedServerSnap];
+ }
+ if (newEventChild != nil) {
+ newEventCache = [self.filter updateChildIn:oldEventSnap.indexedNode
+ forChildKey:childKey
+ newChild:newEventChild
+ affectedPath:childChangePath
+ fromSource:source
+ accumulator:accumulator];
+ } else {
+ // No complete children available or no change
+ newEventCache = oldEventSnap.indexedNode;
+ }
+ }
+ }
+ return [viewCache updateEventSnap:newEventCache
+ isComplete:(oldEventSnap.isFullyInitialized || changePath.isEmpty)
+ isFiltered:self.filter.filtersNodes];
+ }
+}
+
+- (FViewCache *) applyServerOverwriteTo:(FViewCache *)oldViewCache changePath:(FPath *)changePath snap:(id<FNode>)changedSnap
+ writesCache:(FWriteTreeRef *)writesCache completeCache:(id<FNode>)optCompleteCache
+ filterServerNode:(BOOL)filterServerNode accumulator:(FChildChangeAccumulator *)accumulator {
+ FCacheNode *oldServerSnap = oldViewCache.cachedServerSnap;
+ FIndexedNode *newServerCache;
+ id<FNodeFilter> serverFilter = filterServerNode ? self.filter : self.filter.indexedFilter;
+
+ if (changePath.isEmpty) {
+ FIndexedNode *indexed = [FIndexedNode indexedNodeWithNode:changedSnap index:serverFilter.index];
+ newServerCache = [serverFilter updateFullNode:oldServerSnap.indexedNode withNewNode:indexed accumulator:nil];
+ } else if (serverFilter.filtersNodes && !oldServerSnap.isFiltered) {
+ // We want to filter the server node, but we didn't filter the server node yet, so simulate a full update
+ NSAssert(![changePath isEmpty], @"An empty path should been caught in the other branch");
+ NSString *childKey = [changePath getFront];
+ FPath *updatePath = [changePath popFront];
+ id<FNode> newChild = [[oldServerSnap.node getImmediateChild:childKey] updateChild:updatePath
+ withNewChild:changedSnap];
+ FIndexedNode *indexed = [oldServerSnap.indexedNode updateChild:childKey withNewChild:newChild];
+ newServerCache = [serverFilter updateFullNode:oldServerSnap.indexedNode withNewNode:indexed accumulator:nil];
+ } else {
+ NSString *childKey = [changePath getFront];
+ if (![oldServerSnap isCompleteForPath:changePath] && changePath.length > 1) {
+ // We don't update incomplete nodes with updates intended for other listeners.
+ return oldViewCache;
+ }
+ FPath *childChangePath = [changePath popFront];
+ id<FNode> childNode = [oldServerSnap.node getImmediateChild:childKey];
+ id<FNode> newChildNode = [childNode updateChild:childChangePath withNewChild:changedSnap];
+ if ([childKey isEqualToString:@".priority"]) {
+ newServerCache = [serverFilter updatePriority:newChildNode forNode:oldServerSnap.indexedNode];
+ } else {
+ newServerCache = [serverFilter updateChildIn:oldServerSnap.indexedNode
+ forChildKey:childKey
+ newChild:newChildNode
+ affectedPath:childChangePath
+ fromSource:[FNoCompleteChildSource instance]
+ accumulator:nil];
+ }
+ }
+ FViewCache *newViewCache = [oldViewCache updateServerSnap:newServerCache
+ isComplete:(oldServerSnap.isFullyInitialized || changePath.isEmpty)
+ isFiltered:serverFilter.filtersNodes];
+ id<FCompleteChildSource> source = [[FWriteTreeCompleteChildSource alloc] initWithWrites:writesCache
+ viewCache:newViewCache
+ serverCache:optCompleteCache];
+ return [self generateEventCacheAfterServerEvent:newViewCache
+ path:changePath
+ writesCache:writesCache
+ source:source
+ accumulator:accumulator];
+}
+
+- (FViewCache *) applyUserOverwriteTo:(FViewCache *)oldViewCache
+ changePath:(FPath *)changePath
+ changedSnap:(id<FNode>)changedSnap
+ writesCache:(FWriteTreeRef *)writesCache
+ completeCache:(id<FNode>)optCompleteCache
+ accumulator:(FChildChangeAccumulator *)accumulator {
+ FCacheNode *oldEventSnap = oldViewCache.cachedEventSnap;
+ FViewCache *newViewCache;
+ id<FCompleteChildSource> source = [[FWriteTreeCompleteChildSource alloc] initWithWrites:writesCache
+ viewCache:oldViewCache
+ serverCache:optCompleteCache];
+ if (changePath.isEmpty) {
+ FIndexedNode *newIndexed = [FIndexedNode indexedNodeWithNode:changedSnap index:self.filter.index];
+ FIndexedNode *newEventCache = [self.filter updateFullNode:oldEventSnap.indexedNode
+ withNewNode:newIndexed
+ accumulator:accumulator];
+ newViewCache = [oldViewCache updateEventSnap:newEventCache isComplete:YES isFiltered:self.filter.filtersNodes];
+ } else {
+ NSString *childKey = [changePath getFront];
+ if ([childKey isEqualToString:@".priority"]) {
+ FIndexedNode *newEventCache = [self.filter updatePriority:changedSnap
+ forNode:oldViewCache.cachedEventSnap.indexedNode];
+ newViewCache = [oldViewCache updateEventSnap:newEventCache
+ isComplete:oldEventSnap.isFullyInitialized
+ isFiltered:oldEventSnap.isFiltered];
+ } else {
+ FPath *childChangePath = [changePath popFront];
+ id<FNode> oldChild = [oldEventSnap.node getImmediateChild:childKey];
+ id<FNode> newChild;
+ if (childChangePath.isEmpty) {
+ // Child overwrite, we can replace the child
+ newChild = changedSnap;
+ } else {
+ id<FNode> childNode = [source completeChild:childKey];
+ if (childNode != nil) {
+ if ([[childChangePath getBack] isEqualToString:@".priority"] && [childNode getChild:[childChangePath parent]].isEmpty) {
+ // This is a priority update on an empty node. If this node exists on the server, the server
+ // will send down the priority in the update, so ignore for now
+ newChild = childNode;
+ } else {
+ newChild = [childNode updateChild:childChangePath withNewChild:changedSnap];
+ }
+ } else {
+ newChild = [FEmptyNode emptyNode];
+ }
+ }
+ if (![oldChild isEqual:newChild]) {
+ FIndexedNode *newEventSnap = [self.filter updateChildIn:oldEventSnap.indexedNode
+ forChildKey:childKey
+ newChild:newChild
+ affectedPath:childChangePath
+ fromSource:source
+ accumulator:accumulator];
+ newViewCache = [oldViewCache updateEventSnap:newEventSnap isComplete:oldEventSnap.isFullyInitialized isFiltered:self.filter.filtersNodes];
+ } else {
+ newViewCache = oldViewCache;
+ }
+ }
+ }
+ return newViewCache;
+}
+
++ (BOOL) cache:(FViewCache *)viewCache hasChild:(NSString *)childKey {
+ return [viewCache.cachedEventSnap isCompleteForChild:childKey];
+}
+
+/**
+* @param changedChildren NSDictionary of child name (NSString*) to child value (id<FNode>)
+*/
+- (FViewCache *) applyUserMergeTo:(FViewCache *)viewCache
+ path:(FPath *)path
+ changedChildren:(FCompoundWrite *)changedChildren
+ writesCache:(FWriteTreeRef *)writesCache
+ completeCache:(id<FNode>)serverCache
+ accumulator:(FChildChangeAccumulator *)accumulator {
+ // HACK: In the case of a limit query, there may be some changes that bump things out of the
+ // window leaving room for new items. It's important we process these changes first, so we
+ // iterate the changes twice, first processing any that affect items currently in view.
+ // TODO: I consider an item "in view" if cacheHasChild is true, which checks both the server
+ // and event snap. I'm not sure if this will result in edge cases when a child is in one but
+ // not the other.
+ __block FViewCache *curViewCache = viewCache;
+
+ [changedChildren enumerateWrites:^(FPath *relativePath, id<FNode> childNode, BOOL *stop) {
+ FPath *writePath = [path child:relativePath];
+ if ([FViewProcessor cache:viewCache hasChild:[writePath getFront]]) {
+ curViewCache = [self applyUserOverwriteTo:curViewCache
+ changePath:writePath
+ changedSnap:childNode
+ writesCache:writesCache
+ completeCache:serverCache
+ accumulator:accumulator];
+ }
+ }];
+
+ [changedChildren enumerateWrites:^(FPath *relativePath, id<FNode> childNode, BOOL *stop) {
+ FPath *writePath = [path child:relativePath];
+ if (![FViewProcessor cache:viewCache hasChild:[writePath getFront]]) {
+ curViewCache = [self applyUserOverwriteTo:curViewCache
+ changePath:writePath
+ changedSnap:childNode
+ writesCache:writesCache
+ completeCache:serverCache
+ accumulator:accumulator];
+ }
+ }];
+
+ return curViewCache;
+}
+
+- (FViewCache *) applyServerMergeTo:(FViewCache *)viewCache
+ path:(FPath *)path
+ changedChildren:(FCompoundWrite *)changedChildren
+ writesCache:(FWriteTreeRef *)writesCache
+ completeCache:(id<FNode>)serverCache
+ filterServerNode:(BOOL)filterServerNode
+ accumulator:(FChildChangeAccumulator *)accumulator {
+ // If we don't have a cache yet, this merge was intended for a previously listen in the same location. Ignore it and
+ // wait for the complete data update coming soon.
+ if (viewCache.cachedServerSnap.node.isEmpty && !viewCache.cachedServerSnap.isFullyInitialized) {
+ return viewCache;
+ }
+
+ // HACK: In the case of a limit query, there may be some changes that bump things out of the
+ // window leaving room for new items. It's important we process these changes first, so we
+ // iterate the changes twice, first processing any that affect items currently in view.
+ // TODO: I consider an item "in view" if cacheHasChild is true, which checks both the server
+ // and event snap. I'm not sure if this will result in edge cases when a child is in one but
+ // not the other.
+ __block FViewCache *curViewCache = viewCache;
+ FCompoundWrite *actualMerge;
+ if (path.isEmpty) {
+ actualMerge = changedChildren;
+ } else {
+ actualMerge = [[FCompoundWrite emptyWrite] addCompoundWrite:changedChildren atPath:path];
+ }
+ id<FNode> serverNode = viewCache.cachedServerSnap.node;
+
+ NSDictionary *childCompoundWrites = actualMerge.childCompoundWrites;
+ [childCompoundWrites enumerateKeysAndObjectsUsingBlock:^(NSString *childKey, FCompoundWrite *childMerge, BOOL *stop) {
+ if ([serverNode hasChild:childKey]) {
+ id<FNode> serverChild = [viewCache.cachedServerSnap.node getImmediateChild:childKey];
+ id<FNode> newChild = [childMerge applyToNode:serverChild];
+ curViewCache = [self applyServerOverwriteTo:curViewCache
+ changePath:[[FPath alloc] initWith:childKey]
+ snap:newChild
+ writesCache:writesCache
+ completeCache:serverCache
+ filterServerNode:filterServerNode
+ accumulator:accumulator];
+ }
+ }];
+
+ [childCompoundWrites enumerateKeysAndObjectsUsingBlock:^(NSString *childKey, FCompoundWrite *childMerge, BOOL *stop) {
+ bool isUnknownDeepMerge = ![viewCache.cachedServerSnap isCompleteForChild:childKey] && childMerge.rootWrite == nil;
+ if (![serverNode hasChild:childKey] && !isUnknownDeepMerge) {
+ id<FNode> serverChild = [viewCache.cachedServerSnap.node getImmediateChild:childKey];
+ id<FNode> newChild = [childMerge applyToNode:serverChild];
+ curViewCache = [self applyServerOverwriteTo:curViewCache
+ changePath:[[FPath alloc] initWith:childKey]
+ snap:newChild
+ writesCache:writesCache
+ completeCache:serverCache
+ filterServerNode:filterServerNode
+ accumulator:accumulator];
+ }
+ }];
+
+ return curViewCache;
+}
+
+- (FViewCache *) ackUserWriteOn:(FViewCache *)viewCache
+ ackPath:(FPath *)ackPath
+ affectedTree:(FImmutableTree *)affectedTree
+ writesCache:(FWriteTreeRef *)writesCache
+ completeCache:(id <FNode>)optCompleteCache
+ accumulator:(FChildChangeAccumulator *)accumulator {
+
+ if ([writesCache shadowingWriteAtPath:ackPath] != nil) {
+ return viewCache;
+ }
+
+ // Only filter server node if it is currently filtered
+ BOOL filterServerNode = viewCache.cachedServerSnap.isFiltered;
+
+ // Essentially we'll just get our existing server cache for the affected paths and re-apply it as a server update
+ // now that it won't be shadowed.
+ FCacheNode *serverCache = viewCache.cachedServerSnap;
+ if (affectedTree.value != nil) {
+ // This is an overwrite.
+ if ((ackPath.isEmpty && serverCache.isFullyInitialized) || [serverCache isCompleteForPath:ackPath]) {
+ return [self applyServerOverwriteTo:viewCache changePath:ackPath snap:[serverCache.node getChild:ackPath]
+ writesCache:writesCache completeCache:optCompleteCache
+ filterServerNode:filterServerNode accumulator:accumulator];
+ } else if (ackPath.isEmpty) {
+ // This is a goofy edge case where we are acking data at this location but don't have full data. We
+ // should just re-apply whatever we have in our cache as a merge.
+ FCompoundWrite *changedChildren = [FCompoundWrite emptyWrite];
+ for(FNamedNode *child in serverCache.node.childEnumerator) {
+ changedChildren = [changedChildren addWrite:child.node atKey:child.name];
+ }
+ return [self applyServerMergeTo:viewCache path:ackPath changedChildren:changedChildren
+ writesCache:writesCache completeCache:optCompleteCache
+ filterServerNode:filterServerNode accumulator:accumulator];
+ } else {
+ return viewCache;
+ }
+ } else {
+ // This is a merge.
+ __block FCompoundWrite *changedChildren = [FCompoundWrite emptyWrite];
+ [affectedTree forEach:^(FPath *mergePath, id value) {
+ FPath *serverCachePath = [ackPath child:mergePath];
+ if ([serverCache isCompleteForPath:serverCachePath]) {
+ changedChildren = [changedChildren addWrite:[serverCache.node getChild:serverCachePath] atPath:mergePath];
+ }
+ }];
+ return [self applyServerMergeTo:viewCache path:ackPath changedChildren:changedChildren
+ writesCache:writesCache completeCache:optCompleteCache
+ filterServerNode:filterServerNode accumulator:accumulator];
+ }
+}
+
+- (FViewCache *) revertUserWriteOn:(FViewCache *)viewCache
+ path:(FPath *)path
+ writesCache:(FWriteTreeRef *)writesCache
+ completeCache:(id<FNode>)optCompleteCache
+ accumulator:(FChildChangeAccumulator *)accumulator {
+ if ([writesCache shadowingWriteAtPath:path] != nil) {
+ return viewCache;
+ } else {
+ id<FCompleteChildSource> source = [[FWriteTreeCompleteChildSource alloc] initWithWrites:writesCache
+ viewCache:viewCache
+ serverCache:optCompleteCache];
+ FIndexedNode *oldEventCache = viewCache.cachedEventSnap.indexedNode;
+ FIndexedNode *newEventCache;
+ if (path.isEmpty || [[path getFront] isEqualToString:@".priority"]) {
+ id<FNode> newNode;
+ if (viewCache.cachedServerSnap.isFullyInitialized) {
+ newNode = [writesCache calculateCompleteEventCacheWithCompleteServerCache:viewCache.completeServerSnap];
+ } else {
+ newNode = [writesCache calculateCompleteEventChildrenWithCompleteServerChildren:viewCache.cachedServerSnap.node];
+ }
+ FIndexedNode *indexedNode = [FIndexedNode indexedNodeWithNode:newNode index:self.filter.index];
+ newEventCache = [self.filter updateFullNode:oldEventCache withNewNode:indexedNode accumulator:accumulator];
+ } else {
+ NSString *childKey = [path getFront];
+ id<FNode> newChild = [writesCache calculateCompleteChild:childKey cache:viewCache.cachedServerSnap];
+ if (newChild == nil && [viewCache.cachedServerSnap isCompleteForChild:childKey]) {
+ newChild = [oldEventCache.node getImmediateChild:childKey];
+ }
+ if (newChild != nil) {
+ newEventCache = [self.filter updateChildIn:oldEventCache
+ forChildKey:childKey
+ newChild:newChild
+ affectedPath:[path popFront]
+ fromSource:source
+ accumulator:accumulator];
+ } else if (newChild == nil && [viewCache.cachedEventSnap.node hasChild:childKey]) {
+ // No complete child available, delete the existing one, if any
+ newEventCache = [self.filter updateChildIn:oldEventCache
+ forChildKey:childKey
+ newChild:[FEmptyNode emptyNode]
+ affectedPath:[path popFront]
+ fromSource:source
+ accumulator:accumulator];
+ } else {
+ newEventCache = oldEventCache;
+ }
+ if (newEventCache.node.isEmpty && viewCache.cachedServerSnap.isFullyInitialized) {
+ // We might have reverted all child writes. Maybe the old event was a leaf node.
+ id<FNode> complete = [writesCache calculateCompleteEventCacheWithCompleteServerCache:viewCache.completeServerSnap];
+ if (complete.isLeafNode) {
+ FIndexedNode *indexed = [FIndexedNode indexedNodeWithNode:complete];
+ newEventCache = [self.filter updateFullNode:newEventCache
+ withNewNode:indexed
+ accumulator:accumulator];
+ }
+ }
+ }
+ BOOL complete = viewCache.cachedServerSnap.isFullyInitialized || [writesCache shadowingWriteAtPath:[FPath empty]] != nil;
+ return [viewCache updateEventSnap:newEventCache isComplete:complete isFiltered:self.filter.filtersNodes];
+ }
+}
+
+- (FViewCache *) listenCompleteOldCache:(FViewCache *)viewCache
+ path:(FPath *)path
+ writesCache:(FWriteTreeRef *)writesCache
+ serverCache:(id<FNode>)servercache
+ accumulator:(FChildChangeAccumulator *)accumulator {
+ FCacheNode *oldServerNode = viewCache.cachedServerSnap;
+ FViewCache *newViewCache = [viewCache updateServerSnap:oldServerNode.indexedNode
+ isComplete:(oldServerNode.isFullyInitialized || path.isEmpty)
+ isFiltered:oldServerNode.isFiltered];
+ return [self generateEventCacheAfterServerEvent:newViewCache path:path writesCache:writesCache source:[FNoCompleteChildSource instance] accumulator:accumulator];
+}
+
+@end
diff --git a/Firebase/Database/FViewProcessorResult.h b/Firebase/Database/FViewProcessorResult.h
new file mode 100644
index 0000000..e211d19
--- /dev/null
+++ b/Firebase/Database/FViewProcessorResult.h
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+@class FViewCache;
+
+
+@interface FViewProcessorResult : NSObject
+@property (nonatomic, strong, readonly) FViewCache *viewCache;
+/**
+* List of FChanges.
+*/
+@property (nonatomic, strong, readonly) NSArray *changes;
+
+- (id) initWithViewCache:(FViewCache *)viewCache changes:(NSArray *)changes;
+@end
diff --git a/Firebase/Database/FViewProcessorResult.m b/Firebase/Database/FViewProcessorResult.m
new file mode 100644
index 0000000..3327888
--- /dev/null
+++ b/Firebase/Database/FViewProcessorResult.m
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FViewProcessorResult.h"
+#import "FViewCache.h"
+
+@interface FViewProcessorResult ()
+@property (nonatomic, strong, readwrite) FViewCache *viewCache;
+@property (nonatomic, strong, readwrite) NSArray *changes;
+@end
+
+@implementation FViewProcessorResult
+- (id) initWithViewCache:(FViewCache *)viewCache changes:(NSArray *)changes {
+ self = [super init];
+ if (self) {
+ self.viewCache = viewCache;
+ self.changes = changes;
+ }
+ return self;
+}
+
+@end
diff --git a/Firebase/Database/Firebase-Prefix.pch b/Firebase/Database/Firebase-Prefix.pch
new file mode 100644
index 0000000..0158d95
--- /dev/null
+++ b/Firebase/Database/Firebase-Prefix.pch
@@ -0,0 +1,7 @@
+//
+// Prefix header for all source files of the 'Firebase' target in the 'Firebase' project
+//
+
+#ifdef __OBJC__
+ #import <Foundation/Foundation.h>
+#endif
diff --git a/Firebase/Database/FirebaseDatabase.podspec b/Firebase/Database/FirebaseDatabase.podspec
new file mode 100644
index 0000000..4db371e
--- /dev/null
+++ b/Firebase/Database/FirebaseDatabase.podspec
@@ -0,0 +1,48 @@
+# This podspec is not intended to be deployed. It is solely for the static
+# library framework build process at
+# https://github.com/firebase/firebase-ios-sdk/tree/master/BuildFrameworks
+
+Pod::Spec.new do |s|
+ s.name = 'FirebaseDatabase'
+ s.version = '4.0.0'
+ s.summary = 'Firebase Open Source Libraries for iOS.'
+
+ s.description = <<-DESC
+Simplify your iOS development, grow your user base, and monetize more effectively with Firebase.
+ DESC
+
+ s.homepage = 'https://firebase.google.com'
+ s.license = { :type => 'Apache', :file => '../../LICENSE' }
+ s.authors = 'Google, Inc.'
+
+ # NOTE that the FirebaseDev pod is neither publicly deployed nor yet interchangeable with the
+ # Firebase pod
+ s.source = { :git => 'https://github.com/firebase/firebase-ios-sdk.git', :tag => s.version.to_s }
+ s.social_media_url = 'https://twitter.com/Firebase'
+ s.ios.deployment_target = '7.0'
+
+ s.source_files = '**/*.[mh]',
+ 'third_party/Wrap-leveldb/APLevelDB.mm',
+ 'third_party/SocketRocket/fbase64.c'
+ s.public_header_files =
+ 'Api/FirebaseDatabase.h',
+ 'Api/FIRDataEventType.h',
+ 'Api/FIRDataSnapshot.h',
+ 'Api/FIRDatabaseQuery.h',
+ 'Api/FIRDatabaseSwiftNameSupport.h',
+ 'Api/FIRMutableData.h',
+ 'Api/FIRServerValue.h',
+ 'Api/FIRTransactionResult.h',
+ 'Api/FIRDatabase.h',
+ 'FIRDatabaseReference.h'
+ s.library = 'c++'
+ s.library = 'icucore'
+ s.framework = 'CFNetwork'
+ s.framework = 'Security'
+ s.framework = 'SystemConfiguration'
+ s.dependency 'leveldb-library'
+# s.dependency 'FirebaseDev/Core'
+ s.xcconfig = { 'GCC_PREPROCESSOR_DEFINITIONS' =>
+ '$(inherited) ' +
+ 'FIRDatabase_VERSION=' + s.version.to_s }
+end
diff --git a/Firebase/Database/Info.plist b/Firebase/Database/Info.plist
new file mode 100644
index 0000000..c707a67
--- /dev/null
+++ b/Firebase/Database/Info.plist
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>CFBundleDevelopmentRegion</key>
+ <string>en</string>
+ <key>CFBundleExecutable</key>
+ <string>$(EXECUTABLE_NAME)</string>
+ <key>CFBundleIdentifier</key>
+ <string>com.firebase.$(PRODUCT_NAME:rfc1034identifier)</string>
+ <key>CFBundleInfoDictionaryVersion</key>
+ <string>6.0</string>
+ <key>CFBundleName</key>
+ <string>$(PRODUCT_NAME)</string>
+ <key>CFBundlePackageType</key>
+ <string>FMWK</string>
+ <key>CFBundleShortVersionString</key>
+ <string>XXX_TAG_VERSION_XXX</string>
+ <key>CFBundleSignature</key>
+ <string>????</string>
+ <key>CFBundleVersion</key>
+ <string>XXX_TAG_VERSION_XXX</string>
+ <key>NSPrincipalClass</key>
+ <string></string>
+</dict>
+</plist>
diff --git a/Firebase/Database/Login/FAuthTokenProvider.h b/Firebase/Database/Login/FAuthTokenProvider.h
new file mode 100644
index 0000000..dca0026
--- /dev/null
+++ b/Firebase/Database/Login/FAuthTokenProvider.h
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+#import "FTypedefs.h"
+#import "FTypedefs_Private.h"
+
+@protocol FAuthTokenProvider <NSObject>
+
+- (void) fetchTokenForcingRefresh:(BOOL)forceRefresh withCallback:(fbt_void_nsstring_nserror)callback;
+
+- (void) listenForTokenChanges:(fbt_void_nsstring)listener;
+
+@end
+
+@interface FAuthTokenProvider : NSObject
+
++ (id<FAuthTokenProvider>) authTokenProviderForApp:(id)app;
+
+- (instancetype)init NS_UNAVAILABLE;
+
+@end
diff --git a/Firebase/Database/Login/FAuthTokenProvider.m b/Firebase/Database/Login/FAuthTokenProvider.m
new file mode 100644
index 0000000..e406ae7
--- /dev/null
+++ b/Firebase/Database/Login/FAuthTokenProvider.m
@@ -0,0 +1,162 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FAuthTokenProvider.h"
+#import "FUtilities.h"
+#import "FIRDatabaseQuery_Private.h"
+#import "FIRNoopAuthTokenProvider.h"
+
+static NSString *const FIRAuthStateDidChangeInternalNotification = @"FIRAuthStateDidChangeInternalNotification";
+static NSString *const FIRAuthStateDidChangeInternalNotificationTokenKey = @"FIRAuthStateDidChangeInternalNotificationTokenKey";
+
+
+/**
+ * This is a hack that defines all the methods we need from FIRFirebaseApp. At runtime we use reflection to get an
+ * actual instance of FIRFirebaseApp. Since protocols don't carry any runtime information and selectors are invoked
+ * by name we can write code against this protocol as long as the method signatures of FIRFirebaseApp don't change.
+ */
+@protocol FIRFirebaseAppLike <NSObject>
+
+- (void)getTokenForcingRefresh:(BOOL)forceRefresh withCallback:(void (^)(NSString *_Nullable token, NSError *_Nullable error))callback;
+
+@end
+
+
+/**
+ * This is a hack that defines all the methods we need from FIRAuth.
+ */
+@protocol FIRFirebaseAuthLike <NSObject>
+
+- (id<FIRFirebaseAppLike>) app;
+
+@end
+
+/**
+ * This is a hack that copies the definitions of Firebear error codes. If the error codes change in the original code, this
+ * will break at runtime due to undefined behavior!
+ */
+typedef NS_ENUM(NSUInteger, FIRErrorCode) {
+ /*! @var FIRErrorCodeNoAuth
+ @brief Represents the case where an auth-related message was sent to a @c FIRFirebaseApp
+ instance which has no associated @c FIRAuth instance.
+ */
+ FIRErrorCodeNoAuth,
+
+ /*! @var FIRErrorCodeNoSignedInUser
+ @brief Represents the case where an attempt was made to fetch a token when there is no signed
+ in user.
+ */
+ FIRErrorCodeNoSignedInUser,
+};
+
+
+@interface FAuthStateListenerWrapper : NSObject
+
+@property (nonatomic, copy) fbt_void_nsstring listener;
+
+@property (nonatomic, weak) id<FIRFirebaseAppLike> app;
+
+@end
+
+@implementation FAuthStateListenerWrapper
+
+- (instancetype) initWithListener:(fbt_void_nsstring)listener app:(id<FIRFirebaseAppLike>)app {
+ self = [super init];
+ if (self != nil) {
+ self->_listener = listener;
+ self->_app = app;
+ [[NSNotificationCenter defaultCenter] addObserver:self
+ selector:@selector(authStateDidChangeNotification:)
+ name:FIRAuthStateDidChangeInternalNotification
+ object:nil];
+ }
+ return self;
+}
+
+- (void) authStateDidChangeNotification:(NSNotification *)notification {
+ id<FIRFirebaseAuthLike> auth = notification.object;
+ if (auth.app == self->_app) {
+ NSDictionary *userInfo = notification.userInfo;
+ NSString *token = userInfo[FIRAuthStateDidChangeInternalNotificationTokenKey];
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ self.listener(token);
+ });
+ }
+}
+
+- (void) dealloc {
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
+}
+
+@end
+
+
+@interface FIRFirebearAuthTokenProvider : NSObject <FAuthTokenProvider>
+
+@property (nonatomic, strong) id<FIRFirebaseAppLike> app;
+/** Strong references to the auth listeners as they are only weak in FIRFirebaseApp */
+@property (nonatomic, strong) NSMutableArray *authListeners;
+
+- (instancetype) initWithFirebaseApp:(id<FIRFirebaseAppLike>)app;
+
+@end
+
+@implementation FIRFirebearAuthTokenProvider
+
+- (instancetype) initWithFirebaseApp:(id<FIRFirebaseAppLike>)app {
+ self = [super init];
+ if (self != nil) {
+ self->_app = app;
+ self->_authListeners = [NSMutableArray array];
+ }
+ return self;
+}
+
+- (void) fetchTokenForcingRefresh:(BOOL)forceRefresh withCallback:(fbt_void_nsstring_nserror)callback {
+ // TODO: Don't fetch token if there is no current user
+ [self.app getTokenForcingRefresh:forceRefresh withCallback:^(NSString * _Nullable token, NSError * _Nullable error) {
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ if (error != nil) {
+ if (error.code == FIRErrorCodeNoAuth) {
+ FFLog(@"I-RDB073001", @"Firebase Auth is not configured, not going to use authentication.");
+ callback(nil, nil);
+ } else if (error.code == FIRErrorCodeNoSignedInUser) {
+ // No signed in user is an expected case, callback as success with no token
+ callback(nil, nil);
+ } else {
+ callback(nil, error);
+ }
+ } else {
+ callback(token, nil);
+ }
+ });
+ }];
+}
+
+- (void) listenForTokenChanges:(_Nonnull fbt_void_nsstring)listener {
+ FAuthStateListenerWrapper *wrapper = [[FAuthStateListenerWrapper alloc] initWithListener:listener app:self.app];
+ [self.authListeners addObject:wrapper];
+}
+
+@end
+
+@implementation FAuthTokenProvider
+
++ (id<FAuthTokenProvider>) authTokenProviderForApp:(id)app {
+ return [[FIRFirebearAuthTokenProvider alloc] initWithFirebaseApp:app];
+}
+
+@end
diff --git a/Firebase/Database/Login/FIRNoopAuthTokenProvider.h b/Firebase/Database/Login/FIRNoopAuthTokenProvider.h
new file mode 100644
index 0000000..e27ddb4
--- /dev/null
+++ b/Firebase/Database/Login/FIRNoopAuthTokenProvider.h
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+#import "FAuthTokenProvider.h"
+
+@interface FIRNoopAuthTokenProvider : NSObject <FAuthTokenProvider>
+
+@end
diff --git a/Firebase/Database/Login/FIRNoopAuthTokenProvider.m b/Firebase/Database/Login/FIRNoopAuthTokenProvider.m
new file mode 100644
index 0000000..8bf467b
--- /dev/null
+++ b/Firebase/Database/Login/FIRNoopAuthTokenProvider.m
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRNoopAuthTokenProvider.h"
+#import "FAuthTokenProvider.h"
+#import "FIRDatabaseQuery_Private.h"
+
+@implementation FIRNoopAuthTokenProvider
+
+- (void) fetchTokenForcingRefresh:(BOOL)forceRefresh withCallback:(fbt_void_nsstring_nserror)callback {
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ callback(nil, nil);
+ });
+}
+
+- (void) listenForTokenChanges:(fbt_void_nsstring)listener {
+ // no-op, because token never changes
+}
+
+@end
diff --git a/Firebase/Database/Persistence/FCachePolicy.h b/Firebase/Database/Persistence/FCachePolicy.h
new file mode 100644
index 0000000..16b49fb
--- /dev/null
+++ b/Firebase/Database/Persistence/FCachePolicy.h
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+@protocol FCachePolicy <NSObject>
+
+- (BOOL)shouldPruneCacheWithSize:(NSUInteger)cacheSize numberOfTrackedQueries:(NSUInteger)numTrackedQueries;
+- (BOOL)shouldCheckCacheSize:(NSUInteger)serverUpdatesSinceLastCheck;
+- (float)percentOfQueriesToPruneAtOnce;
+- (NSUInteger)maxNumberOfQueriesToKeep;
+
+@end
+
+
+@interface FLRUCachePolicy : NSObject<FCachePolicy>
+
+@property (nonatomic, readonly) NSUInteger maxSize;
+
+- (id)initWithMaxSize:(NSUInteger)maxSize;
+
+@end
+
+@interface FNoCachePolicy : NSObject<FCachePolicy>
+
++ (FNoCachePolicy *)noCachePolicy;
+
+@end
diff --git a/Firebase/Database/Persistence/FCachePolicy.m b/Firebase/Database/Persistence/FCachePolicy.m
new file mode 100644
index 0000000..7da76ef
--- /dev/null
+++ b/Firebase/Database/Persistence/FCachePolicy.m
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FCachePolicy.h"
+
+@interface FLRUCachePolicy ()
+
+@property (nonatomic, readwrite) NSUInteger maxSize;
+
+@end
+
+static const NSUInteger kFServerUpdatesBetweenCacheSizeChecks = 1000;
+static const NSUInteger kFMaxNumberOfPrunableQueriesToKeep = 1000;
+static const float kFPercentOfQueriesToPruneAtOnce = 0.2f;
+
+@implementation FLRUCachePolicy
+
+- (id)initWithMaxSize:(NSUInteger)maxSize {
+ self = [super init];
+ if (self != nil) {
+ self->_maxSize = maxSize;
+ }
+ return self;
+}
+
+- (BOOL)shouldPruneCacheWithSize:(NSUInteger)cacheSize numberOfTrackedQueries:(NSUInteger)numTrackedQueries {
+ return cacheSize > self.maxSize || numTrackedQueries > kFMaxNumberOfPrunableQueriesToKeep;
+}
+
+- (BOOL)shouldCheckCacheSize:(NSUInteger)serverUpdatesSinceLastCheck {
+ return serverUpdatesSinceLastCheck > kFServerUpdatesBetweenCacheSizeChecks;
+}
+
+- (float)percentOfQueriesToPruneAtOnce {
+ return kFPercentOfQueriesToPruneAtOnce;
+}
+
+- (NSUInteger)maxNumberOfQueriesToKeep {
+ return kFMaxNumberOfPrunableQueriesToKeep;
+}
+
+@end
+
+@implementation FNoCachePolicy
+
++ (FNoCachePolicy *)noCachePolicy {
+ return [[FNoCachePolicy alloc] init];
+}
+
+- (BOOL)shouldPruneCacheWithSize:(NSUInteger)cacheSize numberOfTrackedQueries:(NSUInteger)numTrackedQueries {
+ return NO;
+}
+
+- (BOOL)shouldCheckCacheSize:(NSUInteger)serverUpdatesSinceLastCheck {
+ return NO;
+}
+
+- (float)percentOfQueriesToPruneAtOnce {
+ return 0;
+}
+
+- (NSUInteger)maxNumberOfQueriesToKeep {
+ return NSUIntegerMax;
+}
+
+@end
diff --git a/Firebase/Database/Persistence/FLevelDBStorageEngine.h b/Firebase/Database/Persistence/FLevelDBStorageEngine.h
new file mode 100644
index 0000000..059a071
--- /dev/null
+++ b/Firebase/Database/Persistence/FLevelDBStorageEngine.h
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+#import "FStorageEngine.h"
+#import "FNode.h"
+#import "FPath.h"
+#import "FCompoundWrite.h"
+#import "FQuerySpec.h"
+
+@class FCacheNode;
+@class FTrackedQuery;
+@class FPruneForest;
+@class FRepoInfo;
+
+@interface FLevelDBStorageEngine : NSObject<FStorageEngine>
+
+- (id)initWithPath:(NSString *)path;
+
+- (void)runLegacyMigration:(FRepoInfo *)info;
+- (void)purgeEverything;
+
+@end
diff --git a/Firebase/Database/Persistence/FLevelDBStorageEngine.m b/Firebase/Database/Persistence/FLevelDBStorageEngine.m
new file mode 100644
index 0000000..4b324b8
--- /dev/null
+++ b/Firebase/Database/Persistence/FLevelDBStorageEngine.m
@@ -0,0 +1,717 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FLevelDBStorageEngine.h"
+
+#import "APLevelDB.h"
+#import "FSnapshotUtilities.h"
+#import "FWriteRecord.h"
+#import "FTrackedQuery.h"
+#import "FQueryParams.h"
+#import "FEmptyNode.h"
+#import "FPruneForest.h"
+#import "FUtilities.h"
+#import "FPendingPut.h" // For legacy migration
+
+@interface FLevelDBStorageEngine ()
+
+@property (nonatomic, strong) NSString *basePath;
+@property (nonatomic, strong) APLevelDB *writesDB;
+@property (nonatomic, strong) APLevelDB *serverCacheDB;
+
+@end
+
+// WARNING: If you change this, you need to write a migration script
+static NSString * const kFPersistenceVersion = @"1";
+
+static NSString * const kFServerDBPath = @"server_data";
+static NSString * const kFWritesDBPath = @"writes";
+
+static NSString * const kFUserWriteId = @"id";
+static NSString * const kFUserWritePath = @"path";
+static NSString * const kFUserWriteOverwrite = @"o";
+static NSString * const kFUserWriteMerge = @"m";
+
+static NSString * const kFTrackedQueryId = @"id";
+static NSString * const kFTrackedQueryPath = @"path";
+static NSString * const kFTrackedQueryParams = @"p";
+static NSString * const kFTrackedQueryLastUse = @"lu";
+static NSString * const kFTrackedQueryIsComplete = @"c";
+static NSString * const kFTrackedQueryIsActive = @"a";
+
+static NSString * const kFServerCachePrefix = @"/server_cache/";
+// '~' is the last non-control character in the ASCII table until 127
+// We wan't the entire range of thing stored in the DB
+static NSString * const kFServerCacheRangeEnd = @"/server_cache~";
+static NSString * const kFTrackedQueriesPrefix = @"/tracked_queries/";
+static NSString * const kFTrackedQueryKeysPrefix = @"/tracked_query_keys/";
+
+// Failed to load JSON because a valid JSON turns out to be NaN while deserializing
+static const NSInteger kFNanFailureCode = 3840;
+
+static NSString* writeRecordKey(NSUInteger writeId) {
+ return [NSString stringWithFormat:@"%lu", (unsigned long)(writeId)];
+}
+
+static NSString* serverCacheKey(FPath *path) {
+ return [NSString stringWithFormat:@"%@%@", kFServerCachePrefix, ([path toStringWithTrailingSlash])];
+}
+
+static NSString* trackedQueryKey(NSUInteger trackedQueryId) {
+ return [NSString stringWithFormat:@"%@%lu", kFTrackedQueriesPrefix, (unsigned long)trackedQueryId];
+}
+
+static NSString* trackedQueryKeysKeyPrefix(NSUInteger trackedQueryId) {
+ return [NSString stringWithFormat:@"%@%lu/", kFTrackedQueryKeysPrefix, (unsigned long)trackedQueryId];
+}
+
+static NSString* trackedQueryKeysKey(NSUInteger trackedQueryId, NSString *key) {
+ return [NSString stringWithFormat:@"%@%lu/%@", kFTrackedQueryKeysPrefix, (unsigned long)trackedQueryId, key];
+}
+
+@implementation FLevelDBStorageEngine
+#pragma mark - Constructors
+
+- (id)initWithPath:(NSString*)dbPath
+{
+ self = [super init];
+ if (self) {
+ self.basePath = [[FLevelDBStorageEngine firebaseDir] stringByAppendingPathComponent:dbPath];
+ /* For reference:
+ serverDataDB = [aPersistence createDbByName:@"server_data"];
+ FPangolinDB *completenessDb = [aPersistence createDbByName:@"server_complete"];
+ */
+ [FLevelDBStorageEngine ensureDir:self.basePath markAsDoNotBackup:YES];
+ [self runMigration];
+ [self openDatabases];
+ }
+ return self;
+}
+
+- (void)runMigration {
+ // Currently we're at version 1, so all we need to do is write that to a file
+ NSString *versionFile = [self.basePath stringByAppendingPathComponent:@"version"];
+ NSError *error;
+ NSString *oldVersion = [NSString stringWithContentsOfFile:versionFile encoding:NSUTF8StringEncoding error:&error];
+ if (!oldVersion) {
+ // This is probably fine, we don't have a version file yet
+ BOOL success = [kFPersistenceVersion writeToFile:versionFile atomically:NO encoding:NSUTF8StringEncoding error:&error];
+ if (!success) {
+ FFWarn(@"I-RDB076001", @"Failed to write version for database: %@", error);
+ }
+ } else if ([oldVersion isEqualToString:kFPersistenceVersion]) {
+ // Everythings fine no need for migration
+ } else {
+ // If we add more versions in the future, we need to run migration here
+ [NSException raise:NSInternalInconsistencyException format:@"Unrecognized database version: %@", oldVersion];
+ }
+}
+
+- (void)runLegacyMigration:(FRepoInfo *)info {
+ NSArray *dirPaths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
+ NSString *documentsDir = [dirPaths objectAtIndex:0];
+ NSString *firebaseDir = [documentsDir stringByAppendingPathComponent:@"firebase"];
+ NSString* repoHashString = [NSString stringWithFormat:@"%@_%@", info.host, info.namespace];
+ NSString *legacyBaseDir = [NSString stringWithFormat:@"%@/1/%@/v1", firebaseDir, repoHashString];
+ if ([[NSFileManager defaultManager] fileExistsAtPath:legacyBaseDir]) {
+ FFWarn(@"I-RDB076002", @"Legacy database found, migrating...");
+ // We only need to migrate writes
+ NSError *error = nil;
+ APLevelDB *writes = [APLevelDB levelDBWithPath:[legacyBaseDir stringByAppendingPathComponent:@"outstanding_puts"] error:&error];
+ if (writes != nil) {
+ __block NSUInteger numberOfWritesRestored = 0;
+ // Maybe we could use write batches, but what the heck, I'm sure it'll go fine :P
+ [writes enumerateKeysAndValuesAsData:^(NSString *key, NSData *data, BOOL *stop) {
+ id pendingPut = [NSKeyedUnarchiver unarchiveObjectWithData:data];
+ if ([pendingPut isKindOfClass:[FPendingPut class]]) {
+ FPendingPut *put = pendingPut;
+ id<FNode> newNode = [FSnapshotUtilities nodeFrom:put.data priority:put.priority];
+ [self saveUserOverwrite:newNode atPath:put.path writeId:[key integerValue]];
+ numberOfWritesRestored++;
+ } else if ([pendingPut isKindOfClass:[FPendingPutPriority class]]) {
+ // This is for backwards compatibility. Older clients will save FPendingPutPriority. New ones will need to read it and translate.
+ FPendingPutPriority *putPriority = pendingPut;
+ FPath *priorityPath = [putPriority.path childFromString:@".priority"];
+ id<FNode> newNode = [FSnapshotUtilities nodeFrom:putPriority.priority priority:nil];
+ [self saveUserOverwrite:newNode atPath:priorityPath writeId:[key integerValue]];
+ numberOfWritesRestored++;
+ } else if ([pendingPut isKindOfClass:[FPendingUpdate class]]) {
+ FPendingUpdate *update = pendingPut;
+ FCompoundWrite *merge = [FCompoundWrite compoundWriteWithValueDictionary:update.data];
+ [self saveUserMerge:merge atPath:update.path writeId:[key integerValue]];
+ numberOfWritesRestored++;
+ } else {
+ FFWarn(@"I-RDB076003", @"Failed to migrate legacy write, meh!");
+ }
+ }];
+ FFWarn(@"I-RDB076004", @"Migrated %lu writes", (unsigned long)numberOfWritesRestored);
+ [writes close];
+ FFWarn(@"I-RDB076005", @"Deleting legacy database...");
+ BOOL success = [[NSFileManager defaultManager] removeItemAtPath:legacyBaseDir error:&error];
+ if (!success) {
+ FFWarn(@"I-RDB076006", @"Failed to delete legacy database: %@", error);
+ } else {
+ FFWarn(@"I-RDB076007", @"Finished migrating legacy database.");
+ }
+ } else {
+ FFWarn(@"I-RDB076008", @"Failed to migrate old database: %@", error);
+ }
+ }
+}
+
+- (void)openDatabases {
+ self.serverCacheDB = [self createDB:kFServerDBPath];
+ self.writesDB = [self createDB:kFWritesDBPath];
+}
+
+- (void)purgeEverything {
+ [self close];
+ [@[kFServerDBPath, kFWritesDBPath]
+ enumerateObjectsUsingBlock:^(NSString *dbPath, NSUInteger idx, BOOL *stop) {
+ NSString *path = [self.basePath stringByAppendingPathComponent:dbPath];
+ NSError *error;
+ FFDebug(@"I-RDB076009", @"Deleting database at path %@", path);
+ BOOL success = [[NSFileManager defaultManager] removeItemAtPath:path error:&error];
+ if (!success) {
+ [NSException raise:NSInternalInconsistencyException format:@"Failed to delete database files: %@", error];
+ }
+ }];
+
+ [self openDatabases];
+}
+
+- (void)close {
+ // autoreleasepool will cause deallocation which will close the DB
+ @autoreleasepool {
+ [self.serverCacheDB close];
+ self.serverCacheDB = nil;
+ [self.writesDB close];
+ self.writesDB = nil;
+ }
+}
+
++ (NSString *) firebaseDir {
+#if TARGET_OS_IPHONE
+ NSArray *dirPaths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
+ NSString *documentsDir = [dirPaths objectAtIndex:0];
+ return [documentsDir stringByAppendingPathComponent:@"firebase"];
+#else // this must be OSX then
+ return [NSHomeDirectory() stringByAppendingPathComponent:@".firebase"];
+#endif
+}
+
+- (APLevelDB *)createDB:(NSString *)name {
+ NSError *err = nil;
+ NSString *path = [self.basePath stringByAppendingPathComponent:name];
+ APLevelDB *db = [APLevelDB levelDBWithPath:path error:&err];
+ if(err) {
+ NSString *reason = [NSString stringWithFormat:@"Error initializing persistence: %@", [err description]];
+ @throw [NSException exceptionWithName:@"FirebaseDatabasePersistenceFailure" reason:reason userInfo:nil];
+ }
+ return db;
+}
+
+- (void)saveUserOverwrite:(id<FNode>)node atPath:(FPath *)path writeId:(NSUInteger)writeId {
+ NSDictionary *write =
+ @{ kFUserWriteId: @(writeId),
+ kFUserWritePath: [path toStringWithTrailingSlash],
+ kFUserWriteOverwrite: [node valForExport:YES] };
+ NSError *error = nil;
+ NSData *data = [NSJSONSerialization dataWithJSONObject:write options:0 error:&error];
+ NSAssert(data, @"Failed to serialize user overwrite: %@, (Error: %@)", write, error);
+ [self.writesDB setData:data forKey:writeRecordKey(writeId)];
+}
+
+- (void)saveUserMerge:(FCompoundWrite *)merge atPath:(FPath *)path writeId:(NSUInteger)writeId {
+ NSDictionary *write =
+ @{ kFUserWriteId: @(writeId),
+ kFUserWritePath: [path toStringWithTrailingSlash],
+ kFUserWriteMerge: [merge valForExport:YES] };
+ NSError *error = nil;
+ NSData *data = [NSJSONSerialization dataWithJSONObject:write options:0 error:&error];
+ NSAssert(data, @"Failed to serialize user merge: %@ (Error: %@)", write, error);
+ [self.writesDB setData:data forKey:writeRecordKey(writeId)];
+}
+
+- (void)removeUserWrite:(NSUInteger)writeId {
+ [self.writesDB removeKey:writeRecordKey(writeId)];
+}
+
+- (void)removeAllUserWrites {
+ __block NSUInteger count = 0;
+ NSDate *start = [NSDate date];
+ id<APLevelDBWriteBatch> batch = [self.writesDB beginWriteBatch];
+ [self.writesDB enumerateKeys:^(NSString *key, BOOL *stop) {
+ [batch removeKey:key];
+ count++;
+ }];
+ BOOL success = [batch commit];
+ if (!success) {
+ FFWarn(@"I-RDB076010", @"Failed to remove all users writes on disk!");
+ } else {
+ FFDebug(@"I-RDB076011", @"Removed %lu writes in %fms", (unsigned long)count, [start timeIntervalSinceNow]*-1000);
+ }
+}
+
+- (NSArray *)userWrites {
+ NSDate *date = [NSDate date];
+ NSMutableArray *writes = [NSMutableArray array];
+ [self.writesDB enumerateKeysAndValuesAsData:^(NSString *key, NSData *data, BOOL *stop) {
+ NSError *error = nil;
+ NSDictionary *writeJSON = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
+ if (writeJSON == nil) {
+ if (error.code == kFNanFailureCode) {
+ FFWarn(@"I-RDB076012", @"Failed to deserialize write (%@), likely because of out of range doubles (Error: %@)",
+ [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding],
+ error);
+ FFWarn(@"I-RDB076013", @"Removing failed write with key %@", key);
+ [self.writesDB removeKey:key];
+ } else {
+ [NSException raise:NSInternalInconsistencyException format:@"Failed to deserialize write: %@", error];
+ }
+ } else {
+ NSInteger writeId = ((NSNumber *)writeJSON[kFUserWriteId]).integerValue;
+ FPath *path = [FPath pathWithString:writeJSON[kFUserWritePath]];
+ FWriteRecord *writeRecord;
+ if (writeJSON[kFUserWriteMerge] != nil) {
+ // It's a merge
+ FCompoundWrite *merge = [FCompoundWrite compoundWriteWithValueDictionary:writeJSON[kFUserWriteMerge]];
+ writeRecord = [[FWriteRecord alloc] initWithPath:path merge:merge writeId:writeId];
+ } else {
+ // It's an overwrite
+ NSAssert(writeJSON[kFUserWriteOverwrite] != nil, @"Persisted write did not contain merge or overwrite!");
+ id<FNode> node = [FSnapshotUtilities nodeFrom:writeJSON[kFUserWriteOverwrite]];
+ writeRecord = [[FWriteRecord alloc] initWithPath:path overwrite:node writeId:writeId visible:YES];
+ }
+ [writes addObject:writeRecord];
+ }
+ }];
+ // Make sure writes are sorted
+ [writes sortUsingComparator:^NSComparisonResult(FWriteRecord *one, FWriteRecord *two) {
+ if (one.writeId < two.writeId) {
+ return NSOrderedAscending;
+ } else if (one.writeId > two.writeId) {
+ return NSOrderedDescending;
+ } else {
+ return NSOrderedSame;
+ }
+ }];
+ FFDebug(@"I-RDB076014", @"Loaded %lu writes in %fms", (unsigned long)writes.count, [date timeIntervalSinceNow]*-1000);
+ return writes;
+}
+
+- (id<FNode>)serverCacheAtPath:(FPath *)path {
+ NSDate *start = [NSDate date];
+ id data = [self internalNestedDataForPath:path];
+ id<FNode> node = [FSnapshotUtilities nodeFrom:data];
+ FFDebug(@"I-RDB076015", @"Loaded node with %d children at %@ in %fms", [node numChildren], path, [start timeIntervalSinceNow]*-1000);
+ return node;
+}
+
+- (id<FNode>)serverCacheForKeys:(NSSet *)keys atPath:(FPath *)path {
+ NSDate *start = [NSDate date];
+ __block id<FNode> node = [FEmptyNode emptyNode];
+ [keys enumerateObjectsUsingBlock:^(NSString *key, BOOL *stop) {
+ id data = [self internalNestedDataForPath:[path childFromString:key]];
+ node = [node updateImmediateChild:key withNewChild:[FSnapshotUtilities nodeFrom:data]];
+ }];
+ FFDebug(@"I-RDB076016", @"Loaded node with %d children for %lu keys at %@ in %fms", [node numChildren], (unsigned long)keys.count, path, [start timeIntervalSinceNow]*-1000);
+ return node;
+}
+
+- (void)updateServerCache:(id<FNode>)node atPath:(FPath *)path merge:(BOOL)merge {
+ NSDate *start = [NSDate date];
+ id<APLevelDBWriteBatch> batch = [self.serverCacheDB beginWriteBatch];
+ // Remove any leaf nodes that might be higher up
+ [self removeAllLeafNodesOnPath:path batch:batch];
+ __block NSUInteger counter = 0;
+ if (merge) {
+ // remove any children that exist
+ [node enumerateChildrenUsingBlock:^(NSString *childKey, id<FNode> childNode, BOOL *stop) {
+ FPath *childPath = [path childFromString:childKey];
+ [self removeAllWithPrefix:serverCacheKey(childPath) batch:batch database:self.serverCacheDB];
+ [self saveNodeInternal:childNode atPath:childPath batch:batch counter:&counter];
+ }];
+ } else {
+ // remove everything
+ [self removeAllWithPrefix:serverCacheKey(path) batch:batch database:self.serverCacheDB];
+ [self saveNodeInternal:node atPath:path batch:batch counter:&counter];
+ }
+ BOOL success = [batch commit];
+ if (!success) {
+ FFWarn(@"I-RDB076017", @"Failed to update server cache on disk!");
+ } else {
+ FFDebug(@"I-RDB076018", @"Saved %lu leaf nodes for overwrite in %fms", (unsigned long)counter, [start timeIntervalSinceNow]*-1000);
+ }
+}
+
+- (void)updateServerCacheWithMerge:(FCompoundWrite *)merge atPath:(FPath *)path {
+ NSDate *start = [NSDate date];
+ __block NSUInteger counter = 0;
+ id<APLevelDBWriteBatch> batch = [self.serverCacheDB beginWriteBatch];
+ // Remove any leaf nodes that might be higher up
+ [self removeAllLeafNodesOnPath:path batch:batch];
+ [merge enumerateWrites:^(FPath *relativePath, id<FNode> node, BOOL *stop) {
+ FPath *childPath = [path child:relativePath];
+ [self removeAllWithPrefix:serverCacheKey(childPath) batch:batch database:self.serverCacheDB];
+ [self saveNodeInternal:node atPath:childPath batch:batch counter:&counter];
+ }];
+ BOOL success = [batch commit];
+ if (!success) {
+ FFWarn(@"I-RDB076019", @"Failed to update server cache on disk!");
+ } else {
+ FFDebug(@"I-RDB076020", @"Saved %lu leaf nodes for merge in %fms", (unsigned long)counter, [start timeIntervalSinceNow]*-1000);
+ }
+}
+
+- (void)saveNodeInternal:(id<FNode>)node atPath:(FPath *)path batch:(id<APLevelDBWriteBatch>)batch counter:(NSUInteger *)counter {
+ id data = [node valForExport:YES];
+ if(data != nil && ![data isKindOfClass:[NSNull class]]) {
+ [self internalSetNestedData:data forKey:serverCacheKey(path) withBatch:batch counter:counter];
+ }
+}
+
+- (NSUInteger)serverCacheEstimatedSizeInBytes {
+ // Use the exact size, because for pruning the approximate size can lead to weird situations where we prune everything
+ // because no compaction is ever run
+ return [self.serverCacheDB exactSizeFrom:kFServerCachePrefix to:kFServerCacheRangeEnd];
+}
+
+- (void)pruneCache:(FPruneForest *)pruneForest atPath:(FPath *)path {
+ // TODO: be more intelligent, don't scan entire database...
+
+ __block NSUInteger pruned = 0;
+ __block NSUInteger kept = 0;
+ NSDate *start = [NSDate date];
+
+ NSString *prefix = serverCacheKey(path);
+ id<APLevelDBWriteBatch> batch = [self.serverCacheDB beginWriteBatch];
+
+ [self.serverCacheDB enumerateKeysWithPrefix:prefix usingBlock:^(NSString *dbKey, BOOL *stop) {
+ NSString *pathStr = [dbKey substringFromIndex:prefix.length];
+ FPath *relativePath = [[FPath alloc] initWith:pathStr];
+ if ([pruneForest shouldPruneUnkeptDescendantsAtPath:relativePath]) {
+ pruned++;
+ [batch removeKey:dbKey];
+ } else {
+ kept++;
+ }
+ }];
+ BOOL success = [batch commit];
+ if (!success) {
+ FFWarn(@"I-RDB076021", @"Failed to prune cache on disk!");
+ } else {
+ FFDebug(@"I-RDB076022", @"Pruned %lu paths, kept %lu paths in %fms", (unsigned long)pruned, (unsigned long)kept, [start timeIntervalSinceNow]*-1000);
+ }
+}
+
+#pragma mark - Tracked Queries
+
+- (NSArray *)loadTrackedQueries {
+ NSDate *date = [NSDate date];
+ NSMutableArray *trackedQueries = [NSMutableArray array];
+ [self.serverCacheDB enumerateKeysWithPrefix:kFTrackedQueriesPrefix asData:^(NSString *key, NSData *data, BOOL *stop) {
+ NSError *error = nil;
+ NSDictionary *queryJSON = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
+ if (queryJSON == nil) {
+ if (error.code == kFNanFailureCode) {
+ FFWarn(@"I-RDB076023", @"Failed to deserialize tracked query (%@), likely because of out of range doubles (Error: %@)",
+ [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding],
+ error);
+ FFWarn(@"I-RDB076024", @"Removing failed tracked query with key %@", key);
+ [self.serverCacheDB removeKey:key];
+ } else {
+ [NSException raise:NSInternalInconsistencyException format:@"Failed to deserialize tracked query: %@", error];
+ }
+ } else {
+ NSUInteger queryId = ((NSNumber *)queryJSON[kFTrackedQueryId]).unsignedIntegerValue;
+ FPath *path = [FPath pathWithString:queryJSON[kFTrackedQueryPath]];
+ FQueryParams *params = [FQueryParams fromQueryObject:queryJSON[kFTrackedQueryParams]];
+ FQuerySpec *query = [[FQuerySpec alloc] initWithPath:path params:params];
+ BOOL isComplete = [queryJSON[kFTrackedQueryIsComplete] boolValue];
+ BOOL isActive = [queryJSON[kFTrackedQueryIsActive] boolValue];
+ NSTimeInterval lastUse = [queryJSON[kFTrackedQueryLastUse] doubleValue];
+
+ FTrackedQuery *trackedQuery = [[FTrackedQuery alloc] initWithId:queryId
+ query:query
+ lastUse:lastUse
+ isActive:isActive
+ isComplete:isComplete];
+
+ [trackedQueries addObject:trackedQuery];
+ }
+ }];
+ FFDebug(@"I-RDB076025", @"Loaded %lu tracked queries in %fms", (unsigned long)trackedQueries.count, [date timeIntervalSinceNow]*-1000);
+ return trackedQueries;
+}
+
+- (void)removeTrackedQuery:(NSUInteger)queryId {
+ NSDate *start = [NSDate date];
+ id<APLevelDBWriteBatch> batch = [self.serverCacheDB beginWriteBatch];
+ [batch removeKey:trackedQueryKey(queryId)];
+ __block NSUInteger keyCount = 0;
+ [self.serverCacheDB enumerateKeysWithPrefix:trackedQueryKeysKeyPrefix(queryId) usingBlock:^(NSString *key, BOOL *stop) {
+ [batch removeKey:key];
+ keyCount++;
+ }];
+
+ BOOL success = [batch commit];
+ if (!success) {
+ FFWarn(@"I-RDB076026", @"Failed to remove tracked query on disk!");
+ } else {
+ FFDebug(@"I-RDB076027", @"Removed query with id %lu (and removed %lu keys) in %fms",
+ (unsigned long)queryId,
+ (unsigned long)keyCount,
+ [start timeIntervalSinceNow]*-1000);
+ }
+}
+
+- (void)saveTrackedQuery:(FTrackedQuery *)query {
+ NSDate *start = [NSDate date];
+ NSDictionary *trackedQuery =
+ @{
+ kFTrackedQueryId: @(query.queryId),
+ kFTrackedQueryPath: [query.query.path toStringWithTrailingSlash],
+ kFTrackedQueryParams: [query.query.params wireProtocolParams],
+ kFTrackedQueryLastUse: @(query.lastUse),
+ kFTrackedQueryIsComplete: @(query.isComplete),
+ kFTrackedQueryIsActive: @(query.isActive)
+ };
+ NSError *error = nil;
+ NSData *data = [NSJSONSerialization dataWithJSONObject:trackedQuery options:0 error:&error];
+ NSAssert(data, @"Failed to serialize tracked query (Error: %@)", error);
+ [self.serverCacheDB setData:data forKey:trackedQueryKey(query.queryId)];
+ FFDebug(@"I-RDB076028", @"Saved tracked query %lu in %fms", (unsigned long)query.queryId, [start timeIntervalSinceNow]*-1000);
+}
+
+- (void)setTrackedQueryKeys:(NSSet *)keys forQueryId:(NSUInteger)queryId {
+ NSDate *start = [NSDate date];
+ __block NSUInteger removed = 0;
+ __block NSUInteger added = 0;
+ id<APLevelDBWriteBatch> batch = [self.serverCacheDB beginWriteBatch];
+ NSMutableSet *seenKeys = [NSMutableSet set];
+ // First, delete any keys that might be stored and are not part of the current keys
+ [self.serverCacheDB enumerateKeysWithPrefix:trackedQueryKeysKeyPrefix(queryId) asStrings:^(NSString *dbKey, NSString *actualKey, BOOL *stop) {
+ if ([keys containsObject:actualKey]) {
+ // Already in DB
+ [seenKeys addObject:actualKey];
+ } else {
+ // Not part of set, delete key
+ [batch removeKey:dbKey];
+ removed++;
+ }
+ }];
+
+ // Next add any keys that are missing in the database
+ [keys enumerateObjectsUsingBlock:^(NSString *childKey, BOOL *stop) {
+ if (![seenKeys containsObject:childKey]) {
+ [batch setString:childKey forKey:trackedQueryKeysKey(queryId, childKey)];
+ added++;
+ }
+ }];
+ BOOL success = [batch commit];
+ if (!success) {
+ FFWarn(@"I-RDB076029", @"Failed to set tracked queries on disk!");
+ } else {
+ FFDebug(@"I-RDB076030", @"Set %lu tracked keys (%lu added, %lu removed) for query %lu in %fms",
+ (unsigned long)keys.count,
+ (unsigned long)added,
+ (unsigned long)removed,
+ (unsigned long)queryId,
+ [start timeIntervalSinceNow]*-1000);
+ }
+}
+
+- (void)updateTrackedQueryKeysWithAddedKeys:(NSSet *)added removedKeys:(NSSet *)removed forQueryId:(NSUInteger)queryId {
+ NSDate *start = [NSDate date];
+ id<APLevelDBWriteBatch> batch = [self.serverCacheDB beginWriteBatch];
+ [removed enumerateObjectsUsingBlock:^(NSString *key, BOOL *stop) {
+ [batch removeKey:trackedQueryKeysKey(queryId, key)];
+ }];
+ [added enumerateObjectsUsingBlock:^(NSString *key, BOOL *stop) {
+ [batch setString:key forKey:trackedQueryKeysKey(queryId, key)];
+ }];
+ BOOL success = [batch commit];
+ if (!success) {
+ FFWarn(@"I-RDB076031", @"Failed to update tracked queries on disk!");
+ } else {
+ FFDebug(@"I-RDB076032", @"Added %lu tracked keys, removed %lu for query %lu in %fms", (unsigned long)added.count, (unsigned long)removed.count, (unsigned long)queryId, [start timeIntervalSinceNow]*-1000);
+ }
+}
+
+- (NSSet *)trackedQueryKeysForQuery:(NSUInteger)queryId {
+ NSDate *start = [NSDate date];
+ NSMutableSet *set = [NSMutableSet set];
+ [self.serverCacheDB enumerateKeysWithPrefix:trackedQueryKeysKeyPrefix(queryId) asStrings:^(NSString *dbKey, NSString *actualKey, BOOL *stop) {
+ [set addObject:actualKey];
+ }];
+ FFDebug(@"I-RDB076033", @"Loaded %lu tracked keys for query %lu in %fms", (unsigned long)set.count, (unsigned long)queryId, [start timeIntervalSinceNow]*-1000);
+ return set;
+}
+
+#pragma mark - Internal methods
+
+- (void)removeAllLeafNodesOnPath:(FPath *)path batch:(id<APLevelDBWriteBatch>)batch {
+ while (!path.isEmpty) {
+ [batch removeKey:serverCacheKey(path)];
+ path = [path parent];
+ }
+ // Make sure to delete any nodes at the root
+ [batch removeKey:serverCacheKey([FPath empty])];
+}
+
+- (void)removeAllWithPrefix:(NSString *)prefix batch:(id<APLevelDBWriteBatch>)batch database:(APLevelDB *)database {
+ assert(prefix != nil);
+
+ [database enumerateKeysWithPrefix:prefix usingBlock:^(NSString *key, BOOL *stop) {
+ [batch removeKey:key];
+ }];
+}
+
+#pragma mark - Internal helper methods
+
+- (void)internalSetNestedData:(id)value forKey:(NSString *)key withBatch:(id<APLevelDBWriteBatch>)batch counter:(NSUInteger *)counter {
+ if([value isKindOfClass:[NSDictionary class]]) {
+ NSDictionary* dictionary = value;
+ [dictionary enumerateKeysAndObjectsUsingBlock:^(id childKey, id obj, BOOL *stop) {
+ assert(obj != nil);
+ NSString* childPath = [NSString stringWithFormat:@"%@%@/", key, childKey];
+ [self internalSetNestedData:obj forKey:childPath withBatch:batch counter:counter];
+ }];
+ }
+ else {
+ NSData *data = [self serializePrimitive:value];
+ [batch setData:data forKey:key];
+ (*counter)++;
+ }
+}
+
+- (id)internalNestedDataForPath:(FPath *)path {
+ NSAssert(path != nil, @"Path was nil!");
+
+ NSString *baseKey = serverCacheKey(path);
+
+ // HACK to make sure iter is freed now to avoid race conditions (if self.db is deleted before iter, you get an access violation).
+ @autoreleasepool {
+ APLevelDBIterator* iter = [APLevelDBIterator iteratorWithLevelDB:self.serverCacheDB];
+
+ [iter seekToKey:baseKey];
+ if (iter.key == nil || ![iter.key hasPrefix:baseKey]) {
+ // No data.
+ return nil;
+ } else {
+ return [self internalNestedDataFromIterator:iter andKeyPrefix:baseKey];
+ }
+ }
+}
+
+- (id) internalNestedDataFromIterator:(APLevelDBIterator*)iterator andKeyPrefix:(NSString*)prefix {
+ NSString* key = iterator.key;
+
+ if ([key isEqualToString:prefix]) {
+ id result = [self deserializePrimitive:iterator.valueAsData];
+ [iterator nextKey];
+ return result;
+ } else {
+ NSMutableDictionary *dict = [[NSMutableDictionary alloc] init];
+ while (key != nil && [key hasPrefix:prefix]) {
+ NSString *relativePath = [key substringFromIndex:prefix.length];
+ NSArray* pathPieces = [relativePath componentsSeparatedByString:@"/"];
+ assert(pathPieces.count > 0);
+ NSString *childName = pathPieces[0];
+ NSString *childPath = [NSString stringWithFormat:@"%@%@/", prefix, childName];
+ id childValue = [self internalNestedDataFromIterator:iterator andKeyPrefix:childPath];
+ [dict setValue:childValue forKey:childName];
+
+ key = iterator.key;
+ }
+ return dict;
+ }
+}
+
+
+- (NSData*) serializePrimitive:(id)value {
+ // HACK: The built-in serialization only works on dicts and arrays. So we create an array and then strip off
+ // the leading / trailing byte (the [ and ]).
+ NSError *error = nil;
+ NSData *data = [NSJSONSerialization dataWithJSONObject:@[value] options:0 error:&error];
+ NSAssert(data, @"Failed to serialize primitive: %@", error);
+
+ return [data subdataWithRange:NSMakeRange(1, data.length - 2)];
+}
+
+- (id)fixDoubleParsing:(id)value {
+ // The parser for double values in JSONSerialization at the root takes some short-cuts and delivers wrong results
+ // (wrong rounding) for some double values, including 2.47. Because we use the exact bytes for hashing on the server
+ // this will lead to hash mismatches. The parser of NSNumber seems to be more in line with what the server expects,
+ // so we use that here
+ if ([value isKindOfClass:[NSNumber class]]) {
+ CFNumberType type = CFNumberGetType((CFNumberRef)value);
+ if (type == kCFNumberDoubleType || type == kCFNumberFloatType) {
+ // The NSJSON parser returns all numbers as double values, even those that contain no exponent. To
+ // make sure that the String conversion below doesn't unexpectedly reduce precision, we make sure that
+ // our number is indeed not an integer.
+ if ((double)(long long)[value doubleValue] != [value doubleValue]) {
+ NSString *doubleString = [value stringValue];
+ return [NSNumber numberWithDouble:[doubleString doubleValue]];
+ }
+ }
+ }
+ return value;
+}
+
+- (id) deserializePrimitive:(NSData*)data {
+ NSError *error = nil;
+ id result = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:&error];
+ if (result != nil) {
+ return [self fixDoubleParsing:result];
+ } else {
+ if (error.code == kFNanFailureCode) {
+ FFWarn(@"I-RDB076034", @"Failed to load primitive %@, likely because doubles where out of range (Error: %@)",
+ [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding], error);
+ return [NSNull null];
+ } else {
+ [NSException raise:NSInternalInconsistencyException format:@"Failed to deserialiaze primitive: %@", error];
+ return nil;
+ }
+ }
+
+}
+
++ (void)ensureDir:(NSString*)path markAsDoNotBackup:(BOOL)markAsDoNotBackup {
+ NSError* error;
+ BOOL success = [[NSFileManager defaultManager] createDirectoryAtPath:path
+ withIntermediateDirectories:YES
+ attributes:nil
+ error:&error];
+ if (!success) {
+ @throw [NSException exceptionWithName:@"FailedToCreatePersistenceDir" reason:@"Failed to create persistence directory." userInfo:@{ @"path": path }];
+ }
+
+ if (markAsDoNotBackup) {
+ NSURL *firebaseDirURL = [NSURL fileURLWithPath:path];
+ success = [firebaseDirURL setResourceValue:@YES
+ forKey:NSURLIsExcludedFromBackupKey
+ error:&error];
+ if (!success) {
+ FFWarn(@"I-RDB076035", @"Failed to mark firebase database folder as do not backup: %@", error);
+ [NSException raise:@"Error marking as do not backup" format:@"Failed to mark folder %@ as do not backup", firebaseDirURL];
+ }
+ }
+}
+
+
+@end
diff --git a/Firebase/Database/Persistence/FPendingPut.h b/Firebase/Database/Persistence/FPendingPut.h
new file mode 100644
index 0000000..0d8de55
--- /dev/null
+++ b/Firebase/Database/Persistence/FPendingPut.h
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+#import "FPath.h"
+
+// These are all legacy classes and are used to migrate older persistence data base to newer ones
+// These classes should not be used in newer code
+
+@interface FPendingPut : NSObject<NSCoding>
+
+@property (nonatomic, strong) FPath* path;
+@property (nonatomic, strong) id data;
+@property (nonatomic, strong) id priority;
+
+- (id) initWithPath:(FPath*)aPath andData:(id)aData andPriority:aPriority;
+- (void)encodeWithCoder:(NSCoder *)aCoder;
+- (id)initWithCoder:(NSCoder *)aDecoder;
+@end
+
+
+@interface FPendingPutPriority : NSObject<NSCoding>
+
+@property (nonatomic, strong) FPath* path;
+@property (nonatomic, strong) id priority;
+
+- (id) initWithPath:(FPath*)aPath andPriority:(id)aPriority;
+- (void)encodeWithCoder:(NSCoder *)aCoder;
+- (id)initWithCoder:(NSCoder *)aDecoder;
+
+@end
+
+
+@interface FPendingUpdate : NSObject<NSCoding>
+
+@property (nonatomic, strong) FPath* path;
+@property (nonatomic, strong) NSDictionary* data;
+
+- (id) initWithPath:(FPath*)aPath andData:(NSDictionary*)aData;
+- (void)encodeWithCoder:(NSCoder *)aCoder;
+- (id)initWithCoder:(NSCoder *)aDecoder;
+@end
diff --git a/Firebase/Database/Persistence/FPendingPut.m b/Firebase/Database/Persistence/FPendingPut.m
new file mode 100644
index 0000000..12be825
--- /dev/null
+++ b/Firebase/Database/Persistence/FPendingPut.m
@@ -0,0 +1,112 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FPendingPut.h"
+
+@implementation FPendingPut
+
+@synthesize path;
+@synthesize data;
+
+- (id) initWithPath:(FPath *)aPath andData:(id)aData andPriority:(id)aPriority {
+ self = [super init];
+ if (self) {
+ self.path = aPath;
+ self.data = aData;
+ self.priority = aPriority;
+ }
+ return self;
+}
+
+- (void)encodeWithCoder:(NSCoder *)aCoder {
+ [aCoder encodeObject:[self.path description] forKey:@"path"];
+ [aCoder encodeObject:self.data forKey:@"data"];
+ [aCoder encodeObject:self.priority forKey:@"priority"];
+}
+
+- (id)initWithCoder:(NSCoder *)aDecoder {
+ self = [super init];
+ if(self) {
+ self.path = [[FPath alloc] initWith:[aDecoder decodeObjectForKey:@"path"]];
+ self.data = [aDecoder decodeObjectForKey:@"data"];
+ self.priority = [aDecoder decodeObjectForKey:@"priority"];
+ }
+ return self;
+}
+
+@end
+
+
+@implementation FPendingPutPriority
+
+@synthesize path;
+@synthesize priority;
+
+- (id) initWithPath:(FPath *)aPath andPriority:(id)aPriority {
+ self = [super init];
+ if (self) {
+ self.path = aPath;
+ self.priority = aPriority;
+ }
+ return self;
+}
+
+- (void)encodeWithCoder:(NSCoder *)aCoder {
+ [aCoder encodeObject:[self.path description] forKey:@"path"];
+ [aCoder encodeObject:self.priority forKey:@"priority"];
+}
+
+- (id)initWithCoder:(NSCoder *)aDecoder {
+ self = [super init];
+ if(self) {
+ self.path = [[FPath alloc] initWith:[aDecoder decodeObjectForKey:@"path"]];
+ self.priority = [aDecoder decodeObjectForKey:@"priority"];
+ }
+ return self;
+}
+
+@end
+
+
+@implementation FPendingUpdate
+
+@synthesize path;
+@synthesize data;
+
+- (id) initWithPath:(FPath *)aPath andData:(id)aData {
+ self = [super init];
+ if (self) {
+ self.path = aPath;
+ self.data = aData;
+ }
+ return self;
+}
+
+- (void)encodeWithCoder:(NSCoder *)aCoder {
+ [aCoder encodeObject:[self.path description] forKey:@"path"];
+ [aCoder encodeObject:self.data forKey:@"data"];
+}
+
+- (id)initWithCoder:(NSCoder *)aDecoder {
+ self = [super init];
+ if(self) {
+ self.path = [[FPath alloc] initWith:[aDecoder decodeObjectForKey:@"path"]];
+ self.data = [aDecoder decodeObjectForKey:@"data"];
+ }
+ return self;
+}
+
+@end
diff --git a/Firebase/Database/Persistence/FPersistenceManager.h b/Firebase/Database/Persistence/FPersistenceManager.h
new file mode 100644
index 0000000..a3688b3
--- /dev/null
+++ b/Firebase/Database/Persistence/FPersistenceManager.h
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+#import "FNode.h"
+#import "FCompoundWrite.h"
+#import "FQuerySpec.h"
+#import "FRepoInfo.h"
+#import "FStorageEngine.h"
+#import "FCachePolicy.h"
+#import "FCacheNode.h"
+
+@interface FPersistenceManager : NSObject
+
+- (id)initWithStorageEngine:(id<FStorageEngine>)storageEngine cachePolicy:(id<FCachePolicy>)cachePolicy;
+- (void)close;
+
+- (void)saveUserOverwrite:(id<FNode>)node atPath:(FPath *)path writeId:(NSUInteger)writeId;
+- (void)saveUserMerge:(FCompoundWrite *)merge atPath:(FPath *)path writeId:(NSUInteger)writeId;
+- (void)removeUserWrite:(NSUInteger)writeId;
+- (void)removeAllUserWrites;
+- (NSArray *)userWrites;
+
+- (FCacheNode *)serverCacheForQuery:(FQuerySpec *)spec;
+- (void)updateServerCacheWithNode:(id<FNode>)node forQuery:(FQuerySpec *)spec;
+- (void)updateServerCacheWithMerge:(FCompoundWrite *)merge atPath:(FPath *)path;
+
+- (void)applyUserWrite:(id<FNode>)write toServerCacheAtPath:(FPath *)path;
+- (void)applyUserMerge:(FCompoundWrite *)merge toServerCacheAtPath:(FPath *)path;
+
+- (void)setQueryComplete:(FQuerySpec *)spec;
+- (void)setQueryActive:(FQuerySpec *)spec;
+- (void)setQueryInactive:(FQuerySpec *)spec;
+
+- (void)setTrackedQueryKeys:(NSSet *)keys forQuery:(FQuerySpec *)query;
+- (void)updateTrackedQueryKeysWithAddedKeys:(NSSet *)added removedKeys:(NSSet *)removed forQuery:(FQuerySpec *)query;
+
+@end
diff --git a/Firebase/Database/Persistence/FPersistenceManager.m b/Firebase/Database/Persistence/FPersistenceManager.m
new file mode 100644
index 0000000..fb38192
--- /dev/null
+++ b/Firebase/Database/Persistence/FPersistenceManager.m
@@ -0,0 +1,190 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FPersistenceManager.h"
+#import "FLevelDBStorageEngine.h"
+#import "FCacheNode.h"
+#import "FIndexedNode.h"
+#import "FTrackedQueryManager.h"
+#import "FTrackedQuery.h"
+#import "FUtilities.h"
+#import "FPruneForest.h"
+#import "FClock.h"
+
+@interface FPersistenceManager ()
+
+@property (nonatomic, strong) id<FStorageEngine> storageEngine;
+@property (nonatomic, strong) id<FCachePolicy> cachePolicy;
+@property (nonatomic, strong) FTrackedQueryManager *trackedQueryManager;
+@property (nonatomic) NSUInteger serverCacheUpdatesSinceLastPruneCheck;
+
+@end
+
+@implementation FPersistenceManager
+
+- (id)initWithStorageEngine:(id<FStorageEngine>)storageEngine cachePolicy:(id<FCachePolicy>)cachePolicy {
+ self = [super init];
+ if (self != nil) {
+ self->_storageEngine = storageEngine;
+ self->_cachePolicy = cachePolicy;
+ self->_trackedQueryManager = [[FTrackedQueryManager alloc] initWithStorageEngine:self.storageEngine
+ clock:[FSystemClock clock]];
+ }
+ return self;
+}
+
+- (void)close {
+ [self.storageEngine close];
+ self.storageEngine = nil;
+ self.trackedQueryManager = nil;
+}
+
+- (void)saveUserOverwrite:(id<FNode>)node atPath:(FPath *)path writeId:(NSUInteger)writeId {
+ [self.storageEngine saveUserOverwrite:node atPath:path writeId:writeId];
+}
+
+- (void)saveUserMerge:(FCompoundWrite *)merge atPath:(FPath *)path writeId:(NSUInteger)writeId {
+ [self.storageEngine saveUserMerge:merge atPath:path writeId:writeId];
+}
+
+- (void)removeUserWrite:(NSUInteger)writeId {
+ [self.storageEngine removeUserWrite:writeId];
+}
+
+- (void)removeAllUserWrites {
+ [self.storageEngine removeAllUserWrites];
+}
+
+- (NSArray *)userWrites {
+ return [self.storageEngine userWrites];
+}
+
+- (FCacheNode *)serverCacheForQuery:(FQuerySpec *)query {
+ NSSet *trackedKeys;
+ BOOL complete;
+ // TODO[offline]: Should we use trackedKeys to find out if this location is a child of a complete query?
+ if ([self.trackedQueryManager isQueryComplete:query]) {
+ complete = YES;
+ FTrackedQuery *trackedQuery = [self.trackedQueryManager findTrackedQuery:query];
+ if (!query.loadsAllData && trackedQuery.isComplete) {
+ trackedKeys = [self.storageEngine trackedQueryKeysForQuery:trackedQuery.queryId];
+ } else {
+ trackedKeys = nil;
+ }
+ } else {
+ complete = NO;
+ trackedKeys = [self.trackedQueryManager knownCompleteChildrenAtPath:query.path];
+ }
+
+ id<FNode> node;
+ if (trackedKeys != nil) {
+ node = [self.storageEngine serverCacheForKeys:trackedKeys atPath:query.path];
+ } else {
+ node = [self.storageEngine serverCacheAtPath:query.path];
+ }
+
+ FIndexedNode *indexedNode = [FIndexedNode indexedNodeWithNode:node index:query.index];
+ return [[FCacheNode alloc] initWithIndexedNode:indexedNode isFullyInitialized:complete isFiltered:(trackedKeys != nil)];
+}
+
+- (void)updateServerCacheWithNode:(id<FNode>)node forQuery:(FQuerySpec *)query {
+ BOOL merge = !query.loadsAllData;
+ [self.storageEngine updateServerCache:node atPath:query.path merge:merge];
+ [self setQueryComplete:query];
+ [self doPruneCheckAfterServerUpdate];
+}
+
+- (void)updateServerCacheWithMerge:(FCompoundWrite *)merge atPath:(FPath *)path {
+ [self.storageEngine updateServerCacheWithMerge:merge atPath:path];
+ [self doPruneCheckAfterServerUpdate];
+}
+
+- (void)applyUserMerge:(FCompoundWrite *)merge toServerCacheAtPath:(FPath *)path {
+ // TODO[offline]: rework this to be more efficient
+ [merge enumerateWrites:^(FPath *relativePath, id<FNode> node, BOOL *stop) {
+ [self applyUserWrite:node toServerCacheAtPath:[path child:relativePath]];
+ }];
+}
+
+- (void)applyUserWrite:(id<FNode>)write toServerCacheAtPath:(FPath *)path {
+ // This is a hack to guess whether we already cached this because we got a server data update for this
+ // write via an existing active default query. If we didn't, then we'll manually cache this and add a
+ // tracked query to mark it complete and keep it cached.
+ // Unfortunately this is just a guess and it's possible that we *did* get an update (e.g. via a filtered
+ // query) and by overwriting the cache here, we'll actually store an incorrect value (e.g. in the case
+ // that we wrote a ServerValue.TIMESTAMP and the server resolved it to a different value).
+ // TODO[offline]: Consider reworking.
+ if (![self.trackedQueryManager hasActiveDefaultQueryAtPath:path]) {
+ [self.storageEngine updateServerCache:write atPath:path merge:NO];
+ [self.trackedQueryManager ensureCompleteTrackedQueryAtPath:path];
+ }
+}
+
+- (void)setQueryComplete:(FQuerySpec *)query {
+ if (query.loadsAllData) {
+ [self.trackedQueryManager setQueriesCompleteAtPath:query.path];
+ } else {
+ [self.trackedQueryManager setQueryComplete:query];
+ }
+}
+
+- (void)setQueryActive:(FQuerySpec *)spec {
+ [self.trackedQueryManager setQueryActive:spec];
+}
+
+- (void)setQueryInactive:(FQuerySpec *)spec {
+ [self.trackedQueryManager setQueryInactive:spec];
+}
+
+- (void)doPruneCheckAfterServerUpdate {
+ self.serverCacheUpdatesSinceLastPruneCheck++;
+ if ([self.cachePolicy shouldCheckCacheSize:self.serverCacheUpdatesSinceLastPruneCheck]) {
+ FFDebug(@"I-RDB078001", @"Reached prune check threshold. Checking...");
+ NSDate *date = [NSDate date];
+ self.serverCacheUpdatesSinceLastPruneCheck = 0;
+ BOOL canPrune = YES;
+ NSUInteger cacheSize = [self.storageEngine serverCacheEstimatedSizeInBytes];
+ FFDebug(@"I-RDB078002", @"Server cache size: %lu", (unsigned long)cacheSize);
+ while (canPrune && [self.cachePolicy shouldPruneCacheWithSize:cacheSize
+ numberOfTrackedQueries:self.trackedQueryManager.numberOfPrunableQueries]) {
+ FPruneForest *pruneForest = [self.trackedQueryManager pruneOldQueries:self.cachePolicy];
+ if (pruneForest.prunesAnything) {
+ [self.storageEngine pruneCache:pruneForest atPath:[FPath empty]];
+ } else {
+ canPrune = NO;
+ }
+ cacheSize = [self.storageEngine serverCacheEstimatedSizeInBytes];
+ FFDebug(@"I-RDB078003", @"Cache size after pruning: %lu", (unsigned long)cacheSize);
+ }
+ FFDebug(@"I-RDB078004", @"Pruning round took %fms", [date timeIntervalSinceNow]*-1000);
+ }
+}
+
+- (void)setTrackedQueryKeys:(NSSet *)keys forQuery:(FQuerySpec *)query {
+ NSAssert(!query.loadsAllData, @"We should only track keys for filtered queries");
+ FTrackedQuery *trackedQuery = [self.trackedQueryManager findTrackedQuery:query];
+ NSAssert(trackedQuery.isActive, @"We only expect tracked keys for currently-active queries.");
+ [self.storageEngine setTrackedQueryKeys:keys forQueryId:trackedQuery.queryId];
+}
+
+- (void)updateTrackedQueryKeysWithAddedKeys:(NSSet *)added removedKeys:(NSSet *)removed forQuery:(FQuerySpec *)query {
+ NSAssert(!query.loadsAllData, @"We should only track keys for filtered queries");
+ FTrackedQuery *trackedQuery = [self.trackedQueryManager findTrackedQuery:query];
+ NSAssert(trackedQuery.isActive, @"We only expect tracked keys for currently-active queries.");
+ [self.storageEngine updateTrackedQueryKeysWithAddedKeys:added removedKeys:removed forQueryId:trackedQuery.queryId];
+}
+
+@end
diff --git a/Firebase/Database/Persistence/FPruneForest.h b/Firebase/Database/Persistence/FPruneForest.h
new file mode 100644
index 0000000..9e77217
--- /dev/null
+++ b/Firebase/Database/Persistence/FPruneForest.h
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+@class FPath;
+
+@interface FPruneForest : NSObject
+
++ (FPruneForest *)empty;
+
+- (BOOL)prunesAnything;
+- (BOOL)shouldPruneUnkeptDescendantsAtPath:(FPath *)path;
+- (BOOL)shouldKeepPath:(FPath *)path;
+- (BOOL)affectsPath:(FPath *)path;
+- (FPruneForest *)child:(NSString *)childKey;
+- (FPruneForest *)childAtPath:(FPath *)childKey;
+- (FPruneForest *)prunePath:(FPath *)path;
+- (FPruneForest *)keepPath:(FPath *)path;
+- (FPruneForest *)keepAll:(NSSet *)children atPath:(FPath *)path;
+- (FPruneForest *)pruneAll:(NSSet *)children atPath:(FPath *)path;
+
+- (void)enumarateKeptNodesUsingBlock:(void (^)(FPath *path))block;
+
+@end
diff --git a/Firebase/Database/Persistence/FPruneForest.m b/Firebase/Database/Persistence/FPruneForest.m
new file mode 100644
index 0000000..3dae6d8
--- /dev/null
+++ b/Firebase/Database/Persistence/FPruneForest.m
@@ -0,0 +1,177 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FPruneForest.h"
+
+#import "FImmutableTree.h"
+
+@interface FPruneForest ()
+
+@property (nonatomic, strong) FImmutableTree *pruneForest;
+
+@end
+
+@implementation FPruneForest
+
+static BOOL (^kFPrunePredicate)(id) = ^BOOL(NSNumber *pruneValue) {
+ return [pruneValue boolValue];
+};
+
+static BOOL (^kFKeepPredicate)(id) = ^BOOL(NSNumber *pruneValue) {
+ return ![pruneValue boolValue];
+};
+
+
++ (FImmutableTree *)pruneTree {
+ static dispatch_once_t onceToken;
+ static FImmutableTree *pruneTree;
+ dispatch_once(&onceToken, ^{
+ pruneTree = [[FImmutableTree alloc] initWithValue:@YES];
+ });
+ return pruneTree;
+}
+
++ (FImmutableTree *)keepTree {
+ static dispatch_once_t onceToken;
+ static FImmutableTree *keepTree;
+ dispatch_once(&onceToken, ^{
+ keepTree = [[FImmutableTree alloc] initWithValue:@NO];
+ });
+ return keepTree;
+}
+
+- (id) initWithForest:(FImmutableTree *)tree {
+ self = [super init];
+ if (self != nil) {
+ self->_pruneForest = tree;
+ }
+ return self;
+}
+
++ (FPruneForest *)empty {
+ static dispatch_once_t onceToken;
+ static FPruneForest *forest;
+ dispatch_once(&onceToken, ^{
+ forest = [[FPruneForest alloc] initWithForest:[FImmutableTree empty]];
+ });
+ return forest;
+}
+
+- (BOOL)prunesAnything {
+ return [self.pruneForest containsValueMatching:kFPrunePredicate];
+}
+
+- (BOOL)shouldPruneUnkeptDescendantsAtPath:(FPath *)path {
+ NSNumber *shouldPrune = [self.pruneForest leafMostValueOnPath:path];
+ return shouldPrune != nil && [shouldPrune boolValue];
+}
+
+- (BOOL)shouldKeepPath:(FPath *)path {
+ NSNumber *shouldPrune = [self.pruneForest leafMostValueOnPath:path];
+ return shouldPrune != nil && ![shouldPrune boolValue];
+}
+
+- (BOOL)affectsPath:(FPath *)path {
+ return [self.pruneForest rootMostValueOnPath:path] != nil || ![[self.pruneForest subtreeAtPath:path] isEmpty];
+}
+
+- (FPruneForest *)child:(NSString *)childKey {
+ FImmutableTree *childPruneForest = [self.pruneForest.children get:childKey];
+ if (childPruneForest == nil) {
+ if (self.pruneForest.value != nil) {
+ childPruneForest = [self.pruneForest.value boolValue] ? [FPruneForest pruneTree] : [FPruneForest keepTree];
+ } else {
+ childPruneForest = [FImmutableTree empty];
+ }
+ } else {
+ if (childPruneForest.value == nil && self.pruneForest.value != nil) {
+ childPruneForest = [childPruneForest setValue:self.pruneForest.value atPath:[FPath empty]];
+ }
+ }
+ return [[FPruneForest alloc] initWithForest:childPruneForest];
+}
+
+- (FPruneForest *)childAtPath:(FPath *)path {
+ if (path.isEmpty) {
+ return self;
+ } else {
+ return [[self child:path.getFront] childAtPath:[path popFront]];
+ }
+}
+
+- (FPruneForest *)prunePath:(FPath *)path {
+ if ([self.pruneForest rootMostValueOnPath:path matching:kFKeepPredicate]) {
+ [NSException raise:NSInvalidArgumentException format:@"Can't prune path that was kept previously!"];
+ }
+ if ([self.pruneForest rootMostValueOnPath:path matching:kFPrunePredicate]) {
+ // This path will already be pruned
+ return self;
+ } else {
+ FImmutableTree *newPruneForest = [self.pruneForest setTree:[FPruneForest pruneTree] atPath:path];
+ return [[FPruneForest alloc] initWithForest:newPruneForest];
+ }
+}
+
+- (FPruneForest *)keepPath:(FPath *)path {
+ if ([self.pruneForest rootMostValueOnPath:path matching:kFKeepPredicate]) {
+ // This path will already be kept
+ return self;
+ } else {
+ FImmutableTree *newPruneForest = [self.pruneForest setTree:[FPruneForest keepTree] atPath:path];
+ return [[FPruneForest alloc] initWithForest:newPruneForest];
+ }
+}
+
+- (FPruneForest *)keepAll:(NSSet *)children atPath:(FPath *)path {
+ if ([self.pruneForest rootMostValueOnPath:path matching:kFKeepPredicate]) {
+ // This path will already be kept
+ return self;
+ } else {
+ return [self setPruneValue:[FPruneForest keepTree] forAll:children atPath:path];
+ }
+}
+
+- (FPruneForest *)pruneAll:(NSSet *)children atPath:(FPath *)path {
+ if ([self.pruneForest rootMostValueOnPath:path matching:kFKeepPredicate]) {
+ [NSException raise:NSInvalidArgumentException format:@"Can't prune path that was kept previously!"];
+ }
+ if ([self.pruneForest rootMostValueOnPath:path matching:kFPrunePredicate]) {
+ // This path will already be kept
+ return self;
+ } else {
+ return [self setPruneValue:[FPruneForest pruneTree] forAll:children atPath:path];
+ }
+}
+
+- (FPruneForest *)setPruneValue:(FImmutableTree *)pruneValue forAll:(NSSet *)children atPath:(FPath *)path {
+ FImmutableTree *subtree = [self.pruneForest subtreeAtPath:path];
+ __block FImmutableSortedDictionary *childrenDictionary = subtree.children;
+ [children enumerateObjectsUsingBlock:^(NSString *childKey, BOOL *stop) {
+ childrenDictionary = [childrenDictionary insertKey:childKey withValue:pruneValue];
+ }];
+ FImmutableTree *newSubtree = [[FImmutableTree alloc] initWithValue:subtree.value children:childrenDictionary];
+ return [[FPruneForest alloc] initWithForest:[self.pruneForest setTree:newSubtree atPath:path]];
+}
+
+- (void)enumarateKeptNodesUsingBlock:(void (^)(FPath *))block {
+ [self.pruneForest forEach:^(FPath *path, id value) {
+ if (value != nil && ![value boolValue]) {
+ block(path);
+ }
+ }];
+}
+
+@end
diff --git a/Firebase/Database/Persistence/FStorageEngine.h b/Firebase/Database/Persistence/FStorageEngine.h
new file mode 100644
index 0000000..4f168e7
--- /dev/null
+++ b/Firebase/Database/Persistence/FStorageEngine.h
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+@protocol FNode;
+@class FPruneForest;
+@class FPath;
+@class FCompoundWrite;
+@class FQuerySpec;
+@class FTrackedQuery;
+
+@protocol FStorageEngine <NSObject>
+
+- (void)close;
+
+- (void)saveUserOverwrite:(id<FNode>)node atPath:(FPath *)path writeId:(NSUInteger)writeId;
+- (void)saveUserMerge:(FCompoundWrite *)merge atPath:(FPath *)path writeId:(NSUInteger)writeId;
+- (void)removeUserWrite:(NSUInteger)writeId;
+- (void)removeAllUserWrites;
+- (NSArray *)userWrites;
+
+- (id<FNode>)serverCacheAtPath:(FPath *)path;
+- (id<FNode>)serverCacheForKeys:(NSSet *)keys atPath:(FPath *)path;
+- (void)updateServerCache:(id<FNode>)node atPath:(FPath *)path merge:(BOOL)merge;
+- (void)updateServerCacheWithMerge:(FCompoundWrite *)merge atPath:(FPath *)path;
+- (NSUInteger)serverCacheEstimatedSizeInBytes;
+
+- (void)pruneCache:(FPruneForest *)pruneForest atPath:(FPath *)path;
+
+- (NSArray *)loadTrackedQueries;
+- (void)removeTrackedQuery:(NSUInteger)queryId;
+- (void)saveTrackedQuery:(FTrackedQuery *)query;
+
+- (void)setTrackedQueryKeys:(NSSet *)keys forQueryId:(NSUInteger)queryId;
+- (void)updateTrackedQueryKeysWithAddedKeys:(NSSet *)added removedKeys:(NSSet *)removed forQueryId:(NSUInteger)queryId;
+- (NSSet *)trackedQueryKeysForQuery:(NSUInteger)queryId;
+
+
+@end
diff --git a/Firebase/Database/Persistence/FTrackedQuery.h b/Firebase/Database/Persistence/FTrackedQuery.h
new file mode 100644
index 0000000..7bc8ef1
--- /dev/null
+++ b/Firebase/Database/Persistence/FTrackedQuery.h
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+@class FQuerySpec;
+
+@interface FTrackedQuery : NSObject
+
+@property (nonatomic, readonly) NSUInteger queryId;
+@property (nonatomic, strong, readonly) FQuerySpec *query;
+@property (nonatomic, readonly) NSTimeInterval lastUse;
+@property (nonatomic, readonly) BOOL isComplete;
+@property (nonatomic, readonly) BOOL isActive;
+
+- (id)initWithId:(NSUInteger)queryId query:(FQuerySpec *)query lastUse:(NSTimeInterval)lastUse isActive:(BOOL)isActive;
+- (id)initWithId:(NSUInteger)queryId
+ query:(FQuerySpec *)query
+ lastUse:(NSTimeInterval)lastUse
+ isActive:(BOOL)isActive
+ isComplete:(BOOL)isComplete;
+
+- (FTrackedQuery *)updateLastUse:(NSTimeInterval)lastUse;
+- (FTrackedQuery *)setComplete;
+- (FTrackedQuery *)setActiveState:(BOOL)isActive;
+
+@end
diff --git a/Firebase/Database/Persistence/FTrackedQuery.m b/Firebase/Database/Persistence/FTrackedQuery.m
new file mode 100644
index 0000000..1720805
--- /dev/null
+++ b/Firebase/Database/Persistence/FTrackedQuery.m
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FTrackedQuery.h"
+
+#import "FQuerySpec.h"
+
+@interface FTrackedQuery ()
+
+@property (nonatomic, readwrite) NSUInteger queryId;
+@property (nonatomic, strong, readwrite) FQuerySpec *query;
+@property (nonatomic, readwrite) NSTimeInterval lastUse;
+@property (nonatomic, readwrite) BOOL isComplete;
+@property (nonatomic, readwrite) BOOL isActive;
+
+@end
+
+
+@implementation FTrackedQuery
+
+- (id)initWithId:(NSUInteger)queryId
+ query:(FQuerySpec *)query
+ lastUse:(NSTimeInterval)lastUse
+ isActive:(BOOL)isActive
+ isComplete:(BOOL)isComplete {
+ self = [super init];
+ if (self != nil) {
+ self->_queryId = queryId;
+ self->_query = query;
+ self->_lastUse = lastUse;
+ self->_isComplete = isComplete;
+ self->_isActive = isActive;
+ }
+ return self;
+}
+
+- (id)initWithId:(NSUInteger)queryId query:(FQuerySpec *)query lastUse:(NSTimeInterval)lastUse isActive:(BOOL)isActive {
+ return [self initWithId:queryId query:query lastUse:lastUse isActive:isActive isComplete:NO];
+}
+
+- (FTrackedQuery *)updateLastUse:(NSTimeInterval)lastUse {
+ return [[FTrackedQuery alloc] initWithId:self.queryId
+ query:self.query
+ lastUse:lastUse
+ isActive:self.isActive
+ isComplete:self.isComplete];
+}
+
+- (FTrackedQuery *)setComplete {
+ return [[FTrackedQuery alloc] initWithId:self.queryId
+ query:self.query
+ lastUse:self.lastUse
+ isActive:self.isActive
+ isComplete:YES];
+}
+
+- (FTrackedQuery *)setActiveState:(BOOL)isActive {
+ return [[FTrackedQuery alloc] initWithId:self.queryId
+ query:self.query
+ lastUse:self.lastUse
+ isActive:isActive
+ isComplete:self.isComplete];
+}
+
+- (BOOL)isEqual:(id)object {
+ if (![object isKindOfClass:[FTrackedQuery class]]) {
+ return NO;
+ }
+ FTrackedQuery *other = (FTrackedQuery *)object;
+ if (self.queryId != other.queryId) return NO;
+ if (self.query != other.query && ![self.query isEqual:other.query]) return NO;
+ if (self.lastUse != other.lastUse) return NO;
+ if (self.isComplete != other.isComplete) return NO;
+ if (self.isActive != other.isActive) return NO;
+
+ return YES;
+}
+
+- (NSUInteger)hash {
+ NSUInteger hash = self.queryId;
+ hash = hash * 31 + self.query.hash;
+ hash = hash * 31 + (self.isActive ? 1 : 0);
+ hash = hash * 31 + (NSUInteger)self.lastUse;
+ hash = hash * 31 + (self.isComplete ? 1 : 0);
+ hash = hash * 31 + (self.isActive ? 1 : 0);
+ return hash;
+}
+
+@end
diff --git a/Firebase/Database/Persistence/FTrackedQueryManager.h b/Firebase/Database/Persistence/FTrackedQueryManager.h
new file mode 100644
index 0000000..ba2631b
--- /dev/null
+++ b/Firebase/Database/Persistence/FTrackedQueryManager.h
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+@protocol FStorageEngine;
+@protocol FClock;
+@protocol FCachePolicy;
+@class FQuerySpec;
+@class FPath;
+@class FTrackedQuery;
+@class FPruneForest;
+
+@interface FTrackedQueryManager : NSObject
+
+- (id)initWithStorageEngine:(id<FStorageEngine>)storageEngine clock:(id<FClock>)clock;
+
+- (FTrackedQuery *)findTrackedQuery:(FQuerySpec *)query;
+
+- (BOOL)isQueryComplete:(FQuerySpec *)query;
+
+- (void)removeTrackedQuery:(FQuerySpec *)query;
+- (void)setQueryComplete:(FQuerySpec *)query;
+- (void)setQueriesCompleteAtPath:(FPath *)path;
+- (void)setQueryActive:(FQuerySpec *)query;
+- (void)setQueryInactive:(FQuerySpec *)query;
+
+- (BOOL)hasActiveDefaultQueryAtPath:(FPath *)path;
+- (void)ensureCompleteTrackedQueryAtPath:(FPath *)path;
+
+- (FPruneForest *)pruneOldQueries:(id<FCachePolicy>)cachePolicy;
+- (NSUInteger)numberOfPrunableQueries;
+- (NSSet *)knownCompleteChildrenAtPath:(FPath *)path;
+
+// For testing
+- (void)verifyCache;
+
+@end
diff --git a/Firebase/Database/Persistence/FTrackedQueryManager.m b/Firebase/Database/Persistence/FTrackedQueryManager.m
new file mode 100644
index 0000000..bf9753d
--- /dev/null
+++ b/Firebase/Database/Persistence/FTrackedQueryManager.m
@@ -0,0 +1,321 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FTrackedQueryManager.h"
+#import "FImmutableTree.h"
+#import "FLevelDBStorageEngine.h"
+#import "FUtilities.h"
+#import "FTrackedQuery.h"
+#import "FPruneForest.h"
+#import "FClock.h"
+#import "FUtilities.h"
+#import "FCachePolicy.h"
+
+@interface FTrackedQueryManager ()
+
+@property (nonatomic, strong) FImmutableTree *trackedQueryTree;
+@property (nonatomic, strong) id<FStorageEngine> storageEngine;
+@property (nonatomic, strong) id<FClock> clock;
+@property (nonatomic) NSUInteger currentQueryId;
+
+@end
+
+@implementation FTrackedQueryManager
+
+- (id)initWithStorageEngine:(id<FStorageEngine>)storageEngine clock:(id<FClock>)clock {
+ self = [super init];
+ if (self != nil) {
+ self->_storageEngine = storageEngine;
+ self->_clock = clock;
+ self->_trackedQueryTree = [FImmutableTree empty];
+
+ NSTimeInterval lastUse = [clock currentTime];
+
+ NSArray *trackedQueries = [self.storageEngine loadTrackedQueries];
+ [trackedQueries enumerateObjectsUsingBlock:^(FTrackedQuery *trackedQuery, NSUInteger idx, BOOL *stop) {
+ self.currentQueryId = MAX(trackedQuery.queryId + 1, self.currentQueryId);
+ if (trackedQuery.isActive) {
+ trackedQuery = [[trackedQuery setActiveState:NO] updateLastUse:lastUse];
+ FFDebug(@"I-RDB081001", @"Setting active query %lu from previous app start inactive", (unsigned long)trackedQuery.queryId);
+ [self.storageEngine saveTrackedQuery:trackedQuery];
+ }
+ [self cacheTrackedQuery:trackedQuery];
+ }];
+ }
+ return self;
+}
+
++ (void)assertValidTrackedQuery:(FQuerySpec *)query {
+ NSAssert(!query.loadsAllData || query.isDefault, @"Can't have tracked non-default query that loads all data");
+}
+
++ (FQuerySpec *)normalizeQuery:(FQuerySpec *)query {
+ return query.loadsAllData ? [FQuerySpec defaultQueryAtPath:query.path] : query;
+}
+
+- (FTrackedQuery *)findTrackedQuery:(FQuerySpec *)query {
+ query = [FTrackedQueryManager normalizeQuery:query];
+ NSDictionary *set = [self.trackedQueryTree valueAtPath:query.path];
+ return set[query.params];
+}
+
+- (void)removeTrackedQuery:(FQuerySpec *)query {
+ query = [FTrackedQueryManager normalizeQuery:query];
+ FTrackedQuery *trackedQuery = [self findTrackedQuery:query];
+ NSAssert(trackedQuery, @"Tracked query must exist to be removed!");
+
+ [self.storageEngine removeTrackedQuery:trackedQuery.queryId];
+ NSMutableDictionary *trackedQueries = [self.trackedQueryTree valueAtPath:query.path];
+ [trackedQueries removeObjectForKey:query.params];
+}
+
+- (void)setQueryActive:(FQuerySpec *)query {
+ [self setQueryActive:YES forQuery:query];
+}
+
+- (void)setQueryInactive:(FQuerySpec *)query {
+ [self setQueryActive:NO forQuery:query];
+}
+
+- (void)setQueryActive:(BOOL)isActive forQuery:(FQuerySpec *)query {
+ query = [FTrackedQueryManager normalizeQuery:query];
+ FTrackedQuery *trackedQuery = [self findTrackedQuery:query];
+
+ // Regardless of whether it's now active or no langer active, we update the lastUse time
+ NSTimeInterval lastUse = [self.clock currentTime];
+ if (trackedQuery != nil) {
+ trackedQuery = [[trackedQuery updateLastUse:lastUse] setActiveState:isActive];
+ [self.storageEngine saveTrackedQuery:trackedQuery];
+ } else {
+ NSAssert(isActive, @"If we're setting the query to inactive, we should already be tracking it!");
+ trackedQuery = [[FTrackedQuery alloc] initWithId:self.currentQueryId++
+ query:query
+ lastUse:lastUse
+ isActive:isActive];
+ [self.storageEngine saveTrackedQuery:trackedQuery];
+ }
+
+ [self cacheTrackedQuery:trackedQuery];
+}
+
+- (void)setQueryComplete:(FQuerySpec *)query {
+ query = [FTrackedQueryManager normalizeQuery:query];
+ FTrackedQuery *trackedQuery = [self findTrackedQuery:query];
+ if (!trackedQuery) {
+ // We might have removed a query and pruned it before we got the complete message from the server...
+ FFWarn(@"I-RDB081002", @"Trying to set a query complete that is not tracked!");
+ } else if (!trackedQuery.isComplete) {
+ trackedQuery = [trackedQuery setComplete];
+ [self.storageEngine saveTrackedQuery:trackedQuery];
+ [self cacheTrackedQuery:trackedQuery];
+ } else {
+ // Nothing to do, already marked complete
+ }
+}
+
+- (void)setQueriesCompleteAtPath:(FPath *)path {
+ [[self.trackedQueryTree subtreeAtPath:path] forEach:^(FPath *childPath, NSDictionary *trackedQueries) {
+ [trackedQueries enumerateKeysAndObjectsUsingBlock:^(FQueryParams *parms, FTrackedQuery *trackedQuery, BOOL *stop) {
+ if (!trackedQuery.isComplete) {
+ FTrackedQuery *newTrackedQuery = [trackedQuery setComplete];
+ [self.storageEngine saveTrackedQuery:newTrackedQuery];
+ [self cacheTrackedQuery:newTrackedQuery];
+ }
+ }];
+ }];
+}
+
+- (BOOL)isQueryComplete:(FQuerySpec *)query {
+ if ([self isIncludedInDefaultCompleteQuery:query]) {
+ return YES;
+ } else if (query.loadsAllData) {
+ // We didn't find a default complete query, so must not be complete.
+ return NO;
+ } else {
+ NSDictionary *trackedQueries = [self.trackedQueryTree valueAtPath:query.path];
+ return [trackedQueries[query.params] isComplete];
+ }
+}
+
+- (BOOL)hasActiveDefaultQueryAtPath:(FPath *)path {
+ return [self.trackedQueryTree rootMostValueOnPath:path matching:^BOOL(NSDictionary *trackedQueries) {
+ return [trackedQueries[[FQueryParams defaultInstance]] isActive];
+ }] != nil;
+}
+
+- (void)ensureCompleteTrackedQueryAtPath:(FPath *)path {
+ FQuerySpec *query = [FQuerySpec defaultQueryAtPath:path];
+ if (![self isIncludedInDefaultCompleteQuery:query]) {
+ FTrackedQuery *trackedQuery = [self findTrackedQuery:query];
+ if (trackedQuery == nil) {
+ trackedQuery = [[FTrackedQuery alloc] initWithId:self.currentQueryId++
+ query:query
+ lastUse:[self.clock currentTime]
+ isActive:NO
+ isComplete:YES];
+ } else {
+ NSAssert(!trackedQuery.isComplete, @"This should have been handled above!");
+ trackedQuery = [trackedQuery setComplete];
+ }
+ [self.storageEngine saveTrackedQuery:trackedQuery];
+ [self cacheTrackedQuery:trackedQuery];
+ }
+}
+
+- (BOOL)isIncludedInDefaultCompleteQuery:(FQuerySpec *)query {
+ return [self.trackedQueryTree findRootMostMatchingPath:query.path predicate:^BOOL(NSDictionary *trackedQueries) {
+ return [trackedQueries[[FQueryParams defaultInstance]] isComplete];
+ }] != nil;
+}
+
+- (void)cacheTrackedQuery:(FTrackedQuery *)query {
+ [FTrackedQueryManager assertValidTrackedQuery:query.query];
+ NSMutableDictionary *trackedDict = [self.trackedQueryTree valueAtPath:query.query.path];
+ if (trackedDict == nil) {
+ trackedDict = [NSMutableDictionary dictionary];
+ self.trackedQueryTree = [self.trackedQueryTree setValue:trackedDict atPath:query.query.path];
+ }
+ trackedDict[query.query.params] = query;
+}
+
+- (NSUInteger) numberOfQueriesToPrune:(id<FCachePolicy>)cachePolicy prunableCount:(NSUInteger)numPrunable {
+ NSUInteger numPercent = (NSUInteger)ceilf(numPrunable * [cachePolicy percentOfQueriesToPruneAtOnce]);
+ NSUInteger maxToKeep = [cachePolicy maxNumberOfQueriesToKeep];
+ NSUInteger numMax = (numPrunable > maxToKeep) ? numPrunable - maxToKeep : 0;
+ // Make sure we get below number of max queries to prune
+ return MAX(numMax, numPercent);
+}
+
+- (FPruneForest *)pruneOldQueries:(id<FCachePolicy>)cachePolicy {
+ NSMutableArray *pruneableQueries = [NSMutableArray array];
+ NSMutableArray *unpruneableQueries = [NSMutableArray array];
+ [self.trackedQueryTree forEach:^(FPath *path, NSDictionary *trackedQueries) {
+ [trackedQueries enumerateKeysAndObjectsUsingBlock:^(FQueryParams *params, FTrackedQuery *trackedQuery, BOOL *stop) {
+ if (!trackedQuery.isActive) {
+ [pruneableQueries addObject:trackedQuery];
+ } else {
+ [unpruneableQueries addObject:trackedQuery];
+ }
+ }];
+ }];
+ [pruneableQueries sortUsingComparator:^NSComparisonResult(FTrackedQuery *q1, FTrackedQuery *q2) {
+ if (q1.lastUse < q2.lastUse) {
+ return NSOrderedAscending;
+ } else if (q1.lastUse > q2.lastUse) {
+ return NSOrderedDescending;
+ } else {
+ return NSOrderedSame;
+ }
+ }];
+
+
+ __block FPruneForest *pruneForest = [FPruneForest empty];
+ NSUInteger numToPrune = [self numberOfQueriesToPrune:cachePolicy prunableCount:pruneableQueries.count];
+
+ // TODO: do in transaction
+ for (NSUInteger i = 0; i < numToPrune; i++) {
+ FTrackedQuery *toPrune = pruneableQueries[i];
+ pruneForest = [pruneForest prunePath:toPrune.query.path];
+ [self removeTrackedQuery:toPrune.query];
+ }
+
+ // Keep the rest of the prunable queries
+ for (NSUInteger i = numToPrune; i < pruneableQueries.count; i++) {
+ FTrackedQuery *toKeep = pruneableQueries[i];
+ pruneForest = [pruneForest keepPath:toKeep.query.path];
+ }
+
+ // Also keep unprunable queries
+ [unpruneableQueries enumerateObjectsUsingBlock:^(FTrackedQuery *toKeep, NSUInteger idx, BOOL *stop) {
+ pruneForest = [pruneForest keepPath:toKeep.query.path];
+ }];
+
+ return pruneForest;
+}
+
+- (NSUInteger)numberOfPrunableQueries {
+ __block NSUInteger count = 0;
+ [self.trackedQueryTree forEach:^(FPath *path, NSDictionary *trackedQueries) {
+ [trackedQueries enumerateKeysAndObjectsUsingBlock:^(FQueryParams *params, FTrackedQuery *trackedQuery, BOOL *stop) {
+ if (!trackedQuery.isActive) {
+ count++;
+ }
+ }];
+ }];
+ return count;
+}
+
+- (NSSet *)filteredQueryIdsAtPath:(FPath *)path {
+ NSDictionary *queries = [self.trackedQueryTree valueAtPath:path];
+ if (queries) {
+ NSMutableSet *ids = [NSMutableSet set];
+ [queries enumerateKeysAndObjectsUsingBlock:^(FQueryParams *params, FTrackedQuery *query, BOOL *stop) {
+ if (!query.query.loadsAllData) {
+ [ids addObject:@(query.queryId)];
+ }
+ }];
+ return ids;
+ } else {
+ return [NSSet set];
+ }
+}
+
+- (NSSet *)knownCompleteChildrenAtPath:(FPath *)path {
+ NSAssert(![self isQueryComplete:[FQuerySpec defaultQueryAtPath:path]], @"Path is fully complete");
+
+ NSMutableSet *completeChildren = [NSMutableSet set];
+ // First, get complete children from any queries at this location.
+ NSSet *queryIds = [self filteredQueryIdsAtPath:path];
+ [queryIds enumerateObjectsUsingBlock:^(NSNumber *queryId, BOOL *stop) {
+ NSSet *keys = [self.storageEngine trackedQueryKeysForQuery:[queryId unsignedIntegerValue]];
+ [completeChildren unionSet:keys];
+ }];
+
+ // Second, get any complete default queries immediately below us.
+ [[[self.trackedQueryTree subtreeAtPath:path] children] enumerateKeysAndObjectsUsingBlock:^(NSString *childKey, FImmutableTree *childTree, BOOL *stop) {
+ if ([childTree.value[[FQueryParams defaultInstance]] isComplete]) {
+ [completeChildren addObject:childKey];
+ }
+ }];
+
+ return completeChildren;
+}
+
+- (void)verifyCache {
+ NSArray *storedTrackedQueries = [self.storageEngine loadTrackedQueries];
+ NSMutableArray *trackedQueries = [NSMutableArray array];
+
+ [self.trackedQueryTree forEach:^(FPath *path, NSDictionary *queryDict) {
+ [trackedQueries addObjectsFromArray:queryDict.allValues];
+ }];
+ NSComparator comparator = ^NSComparisonResult(FTrackedQuery *q1, FTrackedQuery *q2) {
+ if (q1.queryId < q2.queryId) {
+ return NSOrderedAscending;
+ } else if (q1.queryId > q2.queryId) {
+ return NSOrderedDescending;
+ } else {
+ return NSOrderedSame;
+ }
+ };
+ [trackedQueries sortUsingComparator:comparator];
+ storedTrackedQueries = [storedTrackedQueries sortedArrayUsingComparator:comparator];
+
+ if (![trackedQueries isEqualToArray:storedTrackedQueries]) {
+ [NSException raise:NSInternalInconsistencyException format:@"Tracked queries and queries stored on disk don't match"];
+ }
+}
+
+@end
diff --git a/Firebase/Database/Realtime/FConnection.h b/Firebase/Database/Realtime/FConnection.h
new file mode 100644
index 0000000..ed4879a
--- /dev/null
+++ b/Firebase/Database/Realtime/FConnection.h
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+#import "FWebSocketConnection.h"
+#import "FTypedefs.h"
+
+@protocol FConnectionDelegate;
+
+@interface FConnection : NSObject <FWebSocketDelegate>
+
+@property (nonatomic, weak) id <FConnectionDelegate> delegate;
+
+- (id)initWith:(FRepoInfo *)aRepoInfo andDispatchQueue:(dispatch_queue_t)queue lastSessionID:(NSString *)lastSessionID;
+
+- (void)open;
+- (void)close;
+- (void)sendRequest:(NSDictionary *)dataMsg sensitive:(BOOL)sensitive;
+
+// FWebSocketDelegate delegate methods
+- (void)onMessage:(FWebSocketConnection *)fwebSocket withMessage:(NSDictionary *)message;
+- (void)onDisconnect:(FWebSocketConnection *)fwebSocket wasEverConnected:(BOOL)everConnected;
+
+@end
+
+typedef enum {
+ DISCONNECT_REASON_SERVER_RESET = 0,
+ DISCONNECT_REASON_OTHER = 1
+} FDisconnectReason;
+
+@protocol FConnectionDelegate <NSObject>
+
+- (void)onReady:(FConnection *)fconnection atTime:(NSNumber *)timestamp sessionID:(NSString *)sessionID;
+- (void)onDataMessage:(FConnection *)fconnection withMessage:(NSDictionary *)message;
+- (void)onDisconnect:(FConnection *)fconnection withReason:(FDisconnectReason)reason;
+- (void)onKill:(FConnection *)fconnection withReason:(NSString *)reason;
+
+@end
+
diff --git a/Firebase/Database/Realtime/FConnection.m b/Firebase/Database/Realtime/FConnection.m
new file mode 100644
index 0000000..1550bfc
--- /dev/null
+++ b/Firebase/Database/Realtime/FConnection.m
@@ -0,0 +1,211 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FConnection.h"
+#import "FConstants.h"
+
+typedef enum {
+ REALTIME_STATE_CONNECTING = 0,
+ REALTIME_STATE_CONNECTED = 1,
+ REALTIME_STATE_DISCONNECTED = 2,
+} FConnectionState;
+
+@interface FConnection () {
+ FConnectionState state;
+}
+
+@property (nonatomic, strong) FWebSocketConnection* conn;
+@property (nonatomic, strong) FRepoInfo* repoInfo;
+
+@end
+
+#pragma mark -
+#pragma mark FConnection implementation
+
+@implementation FConnection
+
+@synthesize delegate;
+@synthesize conn;
+@synthesize repoInfo;
+
+#pragma mark -
+#pragma mark Initializers
+
+- (id)initWith:(FRepoInfo *)aRepoInfo andDispatchQueue:(dispatch_queue_t)queue lastSessionID:(NSString *)lastSessionID{
+ self = [super init];
+ if (self) {
+ state = REALTIME_STATE_CONNECTING;
+ self.repoInfo = aRepoInfo;
+ self.conn = [[FWebSocketConnection alloc] initWith:self.repoInfo andQueue:queue lastSessionID:lastSessionID];
+ self.conn.delegate = self;
+ }
+ return self;
+}
+
+#pragma mark -
+#pragma mark Public method implementation
+
+- (void)open {
+ FFLog(@"I-RDB082001", @"Calling open in FConnection");
+ [self.conn open];
+}
+
+- (void) closeWithReason:(FDisconnectReason)reason {
+ if (state != REALTIME_STATE_DISCONNECTED) {
+ FFLog(@"I-RDB082002", @"Closing realtime connection.");
+ state = REALTIME_STATE_DISCONNECTED;
+
+ if (self.conn) {
+ FFLog(@"I-RDB082003", @"Calling close again.");
+ [self.conn close];
+ self.conn = nil;
+ }
+
+ [self.delegate onDisconnect:self withReason:reason];
+ }
+}
+
+- (void) close {
+ [self closeWithReason:DISCONNECT_REASON_OTHER];
+}
+
+- (void) sendRequest:(NSDictionary *)dataMsg sensitive:(BOOL)sensitive {
+ // since this came from the persistent connection, wrap it in a data message envelope
+ NSDictionary* msg = @{
+ kFWPRequestType: kFWPRequestTypeData,
+ kFWPRequestDataPayload: dataMsg
+ };
+ [self sendData:msg sensitive:sensitive];
+}
+
+#pragma mark -
+#pragma mark Helpers
+
+
+- (void) sendData:(NSDictionary *)data sensitive:(BOOL)sensitive {
+ if (state != REALTIME_STATE_CONNECTED) {
+ @throw [[NSException alloc] initWithName:@"InvalidConnectionState" reason:@"Tried to send data on an unconnected FConnection" userInfo:nil];
+ } else {
+ if (sensitive) {
+ FFLog(@"I-RDB082004", @"Sending data (contents hidden)");
+ } else {
+ FFLog(@"I-RDB082005", @"Sending: %@", data);
+ }
+ [self.conn send:data];
+ }
+}
+
+#pragma mark -
+#pragma mark FWebSocketConnectinDelegate implementation
+
+// Corresponds to onConnectionLost in JS
+- (void)onDisconnect:(FWebSocketConnection *)fwebSocket wasEverConnected:(BOOL)everConnected {
+
+ self.conn = nil;
+ if (!everConnected && state == REALTIME_STATE_CONNECTING) {
+ FFLog(@"I-RDB082006", @"Realtime connection failed.");
+
+ // Since we failed to connect at all, clear any cached entry for this namespace in case the machine went away
+ [self.repoInfo clearInternalHostCache];
+ } else if (state == REALTIME_STATE_CONNECTED) {
+ FFLog(@"I-RDB082007", @"Realtime connection lost.");
+ }
+
+ [self close];
+}
+
+// Corresponds to onMessageReceived in JS
+- (void)onMessage:(FWebSocketConnection *)fwebSocket withMessage:(NSDictionary *)message {
+ NSString* rawMessageType = [message objectForKey:kFWPAsyncServerEnvelopeType];
+ if(rawMessageType != nil) {
+ if([rawMessageType isEqualToString:kFWPAsyncServerDataMessage]) {
+ [self onDataMessage:[message objectForKey:kFWPAsyncServerEnvelopeData]];
+ }
+ else if ([rawMessageType isEqualToString:kFWPAsyncServerControlMessage]) {
+ [self onControl:[message objectForKey:kFWPAsyncServerEnvelopeData]];
+ }
+ else {
+ FFLog(@"I-RDB082008", @"Unrecognized server packet type: %@", rawMessageType);
+ }
+ }
+ else {
+ FFLog(@"I-RDB082009", @"Unrecognized raw server packet received: %@", message);
+ }
+}
+
+- (void) onDataMessage:(NSDictionary *)message {
+ // we don't do anything with data messages, just kick them up a level
+ FFLog(@"I-RDB082010", @"Got data message: %@", message);
+ [self.delegate onDataMessage:self withMessage:message];
+}
+
+- (void) onControl:(NSDictionary *)message {
+ FFLog(@"I-RDB082011", @"Got control message: %@", message);
+ NSString* type = [message objectForKey:kFWPAsyncServerControlMessageType];
+ if([type isEqualToString:kFWPAsyncServerControlMessageShutdown]) {
+ NSString* reason = [message objectForKey:kFWPAsyncServerControlMessageData];
+ [self onConnectionShutdownWithReason:reason];
+ }
+ else if ([type isEqualToString:kFWPAsyncServerControlMessageReset]) {
+ NSString* host = [message objectForKey:kFWPAsyncServerControlMessageData];
+ [self onReset:host];
+ }
+ else if ([type isEqualToString:kFWPAsyncServerHello]) {
+ NSDictionary* handshakeData = [message objectForKey:kFWPAsyncServerControlMessageData];
+ [self onHandshake:handshakeData];
+ }
+ else {
+ FFLog(@"I-RDB082012", @"Unknown control message returned from server: %@", message);
+ }
+}
+
+- (void) onConnectionShutdownWithReason:(NSString *)reason {
+ FFLog(@"I-RDB082013", @"Connection shutdown command received. Shutting down...");
+
+ [self.delegate onKill:self withReason:reason];
+ [self close];
+}
+
+- (void) onHandshake:(NSDictionary *)handshake {
+ NSNumber* timestamp = [handshake objectForKey:kFWPAsyncServerHelloTimestamp];
+// NSString* version = [handshake objectForKey:kFWPAsyncServerHelloVersion];
+ NSString* host = [handshake objectForKey:kFWPAsyncServerHelloConnectedHost];
+ NSString* sessionID = [handshake objectForKey:kFWPAsyncServerHelloSession];
+
+ self.repoInfo.internalHost = host;
+
+ if (state == REALTIME_STATE_CONNECTING) {
+ [self.conn start];
+ [self onConnection:self.conn readyAtTime:timestamp sessionID:sessionID];
+ }
+}
+
+- (void) onConnection:(FWebSocketConnection *)conn readyAtTime:(NSNumber *)timestamp sessionID:(NSString *)sessionID {
+ FFLog(@"I-RDB082014", @"Realtime connection established");
+ state = REALTIME_STATE_CONNECTED;
+
+ [self.delegate onReady:self atTime:timestamp sessionID:sessionID];
+}
+
+- (void) onReset:(NSString *)host {
+ FFLog(@"I-RDB082015", @"Got a reset; killing connection to: %@; Updating internalHost to: %@", repoInfo.internalHost, host);
+ self.repoInfo.internalHost = host;
+
+ // Explicitly close the connection with SERVER_RESET so calling code knows to reconnect immediately.
+ [self closeWithReason:DISCONNECT_REASON_SERVER_RESET];
+}
+
+@end
diff --git a/Firebase/Database/Realtime/FWebSocketConnection.h b/Firebase/Database/Realtime/FWebSocketConnection.h
new file mode 100644
index 0000000..6a14d47
--- /dev/null
+++ b/Firebase/Database/Realtime/FWebSocketConnection.h
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+#import "FSRWebSocket.h"
+#import "FUtilities.h"
+
+@protocol FWebSocketDelegate;
+
+@interface FWebSocketConnection : NSObject <FSRWebSocketDelegate>
+
+@property (nonatomic, weak) id <FWebSocketDelegate> delegate;
+
+- (id)initWith:(FRepoInfo *)repoInfo andQueue:(dispatch_queue_t)queue lastSessionID:(NSString *)lastSessionID;
+
+- (void) open;
+- (void) close;
+- (void) start;
+- (void) send:(NSDictionary *)dictionary;
+
+- (void)webSocket:(FSRWebSocket *)webSocket didReceiveMessage:(id)message;
+- (void)webSocketDidOpen:(FSRWebSocket *)webSocket;
+- (void)webSocket:(FSRWebSocket *)webSocket didFailWithError:(NSError *)error;
+- (void)webSocket:(FSRWebSocket *)webSocket didCloseWithCode:(NSInteger)code reason:(NSString *)reason wasClean:(BOOL)wasClean;
+
+@end
+
+@protocol FWebSocketDelegate <NSObject>
+
+- (void)onMessage:(FWebSocketConnection *)fwebSocket withMessage:(NSDictionary *)message;
+- (void)onDisconnect:(FWebSocketConnection *)fwebSocket wasEverConnected:(BOOL)everConnected;
+
+@end
diff --git a/Firebase/Database/Realtime/FWebSocketConnection.m b/Firebase/Database/Realtime/FWebSocketConnection.m
new file mode 100644
index 0000000..52e2296
--- /dev/null
+++ b/Firebase/Database/Realtime/FWebSocketConnection.m
@@ -0,0 +1,305 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Targetted compilation is ONLY for testing. UIKit is weak-linked in actual release build.
+
+#import "FWebSocketConnection.h"
+#import "FConstants.h"
+#import "FIRDatabaseReference.h"
+#import "FStringUtilities.h"
+#import "FIRDatabase_Private.h"
+
+#if TARGET_OS_IPHONE
+#import <UIKit/UIKit.h>
+#endif
+
+@interface FWebSocketConnection () {
+ NSMutableString* frame;
+ BOOL everConnected;
+ BOOL isClosed;
+ NSTimer* keepAlive;
+}
+
+- (void) shutdown;
+- (void) onClosed;
+- (void) closeIfNeverConnected;
+
+@property (nonatomic, strong) FSRWebSocket* webSocket;
+@property (nonatomic, strong) NSNumber* connectionId;
+@property (nonatomic, readwrite) int totalFrames;
+@property (nonatomic, readonly) BOOL buffering;
+@property (nonatomic, readonly) NSString* userAgent;
+@property (nonatomic) dispatch_queue_t dispatchQueue;
+
+- (void)nop:(NSTimer *)timer;
+
+@end
+
+@implementation FWebSocketConnection
+
+@synthesize delegate;
+@synthesize webSocket;
+@synthesize connectionId;
+
+- (id)initWith:(FRepoInfo *)repoInfo andQueue:(dispatch_queue_t)queue lastSessionID:(NSString *)lastSessionID {
+ self = [super init];
+ if (self) {
+ everConnected = NO;
+ isClosed = NO;
+ self.connectionId = [FUtilities LUIDGenerator];
+ self.totalFrames = 0;
+ self.dispatchQueue = queue;
+ frame = nil;
+
+ NSString* connectionUrl = [repoInfo connectionURLWithLastSessionID:lastSessionID];
+ NSString* ua = [self userAgent];
+ FFLog(@"I-RDB083001", @"(wsc:%@) Connecting to: %@ as %@", self.connectionId, connectionUrl, ua);
+
+ NSURLRequest* req = [[NSURLRequest alloc] initWithURL:[[NSURL alloc] initWithString:connectionUrl]];
+ self.webSocket = [[FSRWebSocket alloc] initWithURLRequest:req queue:queue andUserAgent:ua];
+ [self.webSocket setDelegateDispatchQueue:queue];
+ self.webSocket.delegate = self;
+ }
+ return self;
+}
+
+- (NSString *) userAgent {
+ NSString* systemVersion;
+ NSString* deviceName;
+ BOOL hasUiDeviceClass = NO;
+
+ // Targetted compilation is ONLY for testing. UIKit is weak-linked in actual release build.
+ #if TARGET_OS_IPHONE
+ Class uiDeviceClass = NSClassFromString(@"UIDevice");
+ if (uiDeviceClass) {
+ systemVersion = [uiDeviceClass currentDevice].systemVersion;
+ deviceName = [uiDeviceClass currentDevice].model;
+ hasUiDeviceClass = YES;
+ }
+ #endif
+
+ if (!hasUiDeviceClass) {
+ NSDictionary *systemVersionDictionary = [NSDictionary dictionaryWithContentsOfFile:@"/System/Library/CoreServices/SystemVersion.plist"];
+ systemVersion = [systemVersionDictionary objectForKey:@"ProductVersion"];
+ deviceName = [systemVersionDictionary objectForKey:@"ProductName"];
+ }
+
+ NSString* bundleIdentifier = [[NSBundle mainBundle] bundleIdentifier];
+
+ // Sanitize '/'s in deviceName and bundleIdentifier for stats
+ deviceName = [FStringUtilities sanitizedForUserAgent:deviceName];
+ bundleIdentifier = [FStringUtilities sanitizedForUserAgent:bundleIdentifier];
+
+ // Firebase/5/<semver>_<build date>_<git hash>/<os version>/{device model / os (Mac OS X, iPhone, etc.}_<bundle id>
+ NSString* ua = [NSString stringWithFormat:@"Firebase/%@/%@/%@/%@_%@", kWebsocketProtocolVersion, [FIRDatabase buildVersion], systemVersion, deviceName, bundleIdentifier];
+ return ua;
+}
+
+- (BOOL) buffering {
+ return frame != nil;
+}
+
+#pragma mark -
+#pragma mark Public FWebSocketConnection methods
+
+- (void) open {
+ FFLog(@"I-RDB083002", @"(wsc:%@) FWebSocketConnection open.", self.connectionId);
+ assert(delegate);
+ everConnected = NO;
+ // TODO Assert url
+ [self.webSocket open];
+ dispatch_time_t when = dispatch_time(DISPATCH_TIME_NOW, kWebsocketConnectTimeout * NSEC_PER_SEC);
+ dispatch_after(when, self.dispatchQueue, ^{
+ [self closeIfNeverConnected];
+ });
+}
+
+- (void) close {
+ FFLog(@"I-RDB083003", @"(wsc:%@) FWebSocketConnection is being closed.", self.connectionId);
+ isClosed = YES;
+ [self.webSocket close];
+}
+
+- (void) start {
+ // Start is a no-op for websockets.
+}
+
+- (void) send:(NSDictionary *)dictionary {
+
+ [self resetKeepAlive];
+
+ NSData* jsonData = [NSJSONSerialization dataWithJSONObject:dictionary
+ options:kNilOptions error:nil];
+
+ NSString* data = [[NSString alloc] initWithData:jsonData
+ encoding:NSUTF8StringEncoding];
+
+ NSArray* dataSegs = [FUtilities splitString:data intoMaxSize:kWebsocketMaxFrameSize];
+
+ // First send the header so the server knows how many segments are forthcoming
+ if (dataSegs.count > 1) {
+ [self.webSocket send:[NSString stringWithFormat:@"%u", (unsigned int)dataSegs.count]];
+ }
+
+ // Then, actually send the segments.
+ for(NSString * segment in dataSegs) {
+ [self.webSocket send:segment];
+ }
+}
+
+- (void) nop:(NSTimer *)timer {
+ if(self.webSocket) {
+ FFLog(@"I-RDB083004", @"(wsc:%@) nop", self.connectionId);
+ [self.webSocket send:@"0"];
+ }
+ else {
+ FFLog(@"I-RDB083005", @"(wsc:%@) No more websocket; invalidating nop timer.", self.connectionId);
+ [timer invalidate];
+ }
+}
+
+- (void) handleNewFrameCount:(int) numFrames {
+ self.totalFrames = numFrames;
+ frame = [[NSMutableString alloc] initWithString:@""];
+ FFLog(@"I-RDB083006", @"(wsc:%@) handleNewFrameCount: %d", self.connectionId, self.totalFrames);
+}
+
+- (NSString *) extractFrameCount:(NSString *) message {
+ if ([message length] <= 4) {
+ int frameCount = [message intValue];
+ if (frameCount > 0) {
+ [self handleNewFrameCount:frameCount];
+ return nil;
+ }
+ }
+ [self handleNewFrameCount:1];
+ return message;
+}
+
+- (void) appendFrame:(NSString *) message {
+ [frame appendString:message];
+ self.totalFrames = self.totalFrames - 1;
+
+ if (self.totalFrames == 0) {
+ // Call delegate and pass an immutable version of the frame
+ NSDictionary* json = [NSJSONSerialization JSONObjectWithData:[frame dataUsingEncoding:NSUTF8StringEncoding]
+ options:kNilOptions
+ error:nil];
+ frame = nil;
+ FFLog(@"I-RDB083007", @"(wsc:%@) handleIncomingFrame sending complete frame: %d", self.connectionId, self.totalFrames);
+
+ @autoreleasepool {
+ [self.delegate onMessage:self withMessage:json];
+ }
+ }
+}
+
+- (void) handleIncomingFrame:(NSString *) message {
+ [self resetKeepAlive];
+ if (self.buffering) {
+ [self appendFrame:message];
+ } else {
+ NSString *remaining = [self extractFrameCount:message];
+ if (remaining) {
+ [self appendFrame:remaining];
+ }
+ }
+}
+
+#pragma mark -
+#pragma mark SRWebSocketDelegate implementation
+- (void)webSocket:(FSRWebSocket *)webSocket didReceiveMessage:(id)message
+{
+ [self handleIncomingFrame:message];
+}
+
+- (void)webSocketDidOpen:(FSRWebSocket *)webSocket
+{
+ FFLog(@"I-RDB083008", @"(wsc:%@) webSocketDidOpen", self.connectionId);
+
+ everConnected = YES;
+
+ dispatch_async(dispatch_get_main_queue(), ^{
+ self->keepAlive = [NSTimer scheduledTimerWithTimeInterval:kWebsocketKeepaliveInterval
+ target:self
+ selector:@selector(nop:)
+ userInfo:nil
+ repeats:YES];
+ FFLog(@"I-RDB083009", @"(wsc:%@) nop timer kicked off", self.connectionId);
+ });
+}
+
+- (void)webSocket:(FSRWebSocket *)webSocket didFailWithError:(NSError *)error
+{
+ FFLog(@"I-RDB083010", @"(wsc:%@) didFailWithError didFailWithError: %@", self.connectionId, [error description]);
+ [self onClosed];
+}
+
+- (void)webSocket:(FSRWebSocket *)webSocket didCloseWithCode:(NSInteger)code reason:(NSString *)reason wasClean:(BOOL)wasClean
+{
+ FFLog(@"I-RDB083011", @"(wsc:%@) didCloseWithCode: %ld %@", self.connectionId, (long)code, reason);
+ [self onClosed];
+}
+
+#pragma mark -
+#pragma mark Private methods
+
+/**
+ * Note that the close / onClosed / shutdown cycle here is a little different from the javascript client.
+ * In order to properly handle deallocation, no close-related action is taken at a higher level until we
+ * have received notification from the websocket itself that it is closed. Otherwise, we end up deallocating
+ * this class and the FConnection class before the websocket has a change to call some of its delegate methods.
+ * So, since close is the external close handler, we just set a flag saying not to call our own delegate method
+ * and close the websocket. That will trigger a callback into this class that can then do things like clean up
+ * the keepalive timer.
+ */
+
+- (void) closeIfNeverConnected {
+ if (!everConnected) {
+ FFLog(@"I-RDB083012", @"(wsc:%@) Websocket timed out on connect", self.connectionId);
+ [self.webSocket close];
+ }
+}
+
+- (void) shutdown {
+ isClosed = YES;
+
+ // Call delegate methods
+ [self.delegate onDisconnect:self wasEverConnected:everConnected];
+
+}
+
+- (void) onClosed {
+ if (!isClosed) {
+ FFLog(@"I-RDB083013", @"Websocket is closing itself");
+ [self shutdown];
+ }
+ self.webSocket = nil;
+ if (keepAlive.isValid) {
+ [keepAlive invalidate];
+ }
+}
+
+- (void) resetKeepAlive {
+ NSDate* newTime = [NSDate dateWithTimeIntervalSinceNow:kWebsocketKeepaliveInterval];
+ // Calling setFireDate is actually kinda' expensive, so wait at least 5 seconds before updating it.
+ if ([newTime timeIntervalSinceDate:keepAlive.fireDate] > 5) {
+ FFLog(@"I-RDB083014", @"(wsc:%@) resetting keepalive, to %@ ; old: %@", self.connectionId, newTime, [keepAlive fireDate]);
+ [keepAlive setFireDate:newTime];
+ }
+}
+
+@end
diff --git a/Firebase/Database/Snapshot/FChildrenNode.h b/Firebase/Database/Snapshot/FChildrenNode.h
new file mode 100644
index 0000000..9eebdae
--- /dev/null
+++ b/Firebase/Database/Snapshot/FChildrenNode.h
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+#import "FNode.h"
+#import "FTypedefs.h"
+#import "FTypedefs_Private.h"
+#import "FImmutableSortedDictionary.h"
+
+@class FNamedNode;
+
+@interface FChildrenNode : NSObject <FNode>
+
+- (id)initWithChildren:(FImmutableSortedDictionary *)someChildren;
+- (id)initWithPriority:(id<FNode>)aPriority children:(FImmutableSortedDictionary *)someChildren;
+
+// FChildrenNode specific methods
+
+- (void) enumerateChildrenAndPriorityUsingBlock:(void (^)(NSString *, id<FNode>, BOOL *))block;
+
+- (FNamedNode *) firstChild;
+- (FNamedNode *) lastChild;
+
+@property (nonatomic, strong) FImmutableSortedDictionary* children;
+@property (nonatomic, strong) id<FNode> priorityNode;
+
+@end
diff --git a/Firebase/Database/Snapshot/FChildrenNode.m b/Firebase/Database/Snapshot/FChildrenNode.m
new file mode 100644
index 0000000..b5598ad
--- /dev/null
+++ b/Firebase/Database/Snapshot/FChildrenNode.m
@@ -0,0 +1,385 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FChildrenNode.h"
+#import "FEmptyNode.h"
+#import "FConstants.h"
+#import "FStringUtilities.h"
+#import "FUtilities.h"
+#import "FNamedNode.h"
+#import "FMaxNode.h"
+#import "FTransformedEnumerator.h"
+#import "FSnapshotUtilities.h"
+#import "FTransformedEnumerator.h"
+#import "FPriorityIndex.h"
+#import "FUtilities.h"
+
+@interface FChildrenNode ()
+@property (nonatomic, strong) NSString *lazyHash;
+@end
+
+@implementation FChildrenNode
+
+// Note: The only reason we allow nil priority is to for EmptyNode, since we can't use
+// EmptyNode as the priority of EmptyNode. We might want to consider making EmptyNode its own
+// class instead of an empty ChildrenNode.
+
+- (id)init {
+ return [self initWithPriority:nil children:[FImmutableSortedDictionary dictionaryWithComparator:[FUtilities keyComparator]]];
+}
+
+- (id)initWithChildren:(FImmutableSortedDictionary *)someChildren {
+ return [self initWithPriority:nil children:someChildren];
+}
+
+- (id)initWithPriority:(id<FNode>)aPriority children:(FImmutableSortedDictionary *)someChildren {
+ if (someChildren.isEmpty && aPriority != nil && ![aPriority isEmpty]) {
+ [NSException raise:NSInvalidArgumentException format:@"Can't create empty node with priority!"];
+ }
+ self = [super init];
+ if(self) {
+ self.children = someChildren;
+ self.priorityNode = aPriority;
+ }
+ return self;
+}
+
+- (NSString *) description {
+ return [[self valForExport:YES] description];
+}
+
+#pragma mark -
+#pragma mark FNode methods
+
+
+- (BOOL) isLeafNode {
+ return NO;
+}
+
+- (id<FNode>) getPriority {
+ if (self.priorityNode) {
+ return self.priorityNode;
+ } else {
+ return [FEmptyNode emptyNode];
+ }
+
+}
+
+- (id<FNode>) updatePriority:(id<FNode>)aPriority {
+ if ([self.children isEmpty]) {
+ return [FEmptyNode emptyNode];
+ } else {
+ return [[FChildrenNode alloc] initWithPriority:aPriority children:self.children];
+ }
+}
+
+- (id<FNode>) getImmediateChild:(NSString *) childName {
+ if ([childName isEqualToString:@".priority"]) {
+ return [self getPriority];
+ } else {
+ id <FNode> child = [self.children objectForKey:childName];
+ return (child == nil) ? [FEmptyNode emptyNode] : child;
+ }
+}
+
+- (id<FNode>) getChild:(FPath *)path {
+ NSString* front = [path getFront];
+ if(front == nil) {
+ return self;
+ }
+ else {
+ return [[self getImmediateChild:front] getChild:[path popFront]];
+ }
+}
+
+- (BOOL)hasChild:(NSString *)childName {
+ return ![self getImmediateChild:childName].isEmpty;
+}
+
+
+- (id<FNode>) updateImmediateChild:(NSString *)childName withNewChild:(id<FNode>)newChildNode {
+ NSAssert(newChildNode != nil, @"Should always be passing nodes.");
+
+ if ([childName isEqualToString:@".priority"]) {
+ return [self updatePriority:newChildNode];
+ } else {
+ FImmutableSortedDictionary *newChildren;
+ if (newChildNode.isEmpty) {
+ newChildren = [self.children removeObjectForKey:childName];
+ } else {
+ newChildren = [self.children setObject:newChildNode forKey:childName];
+ }
+ if (newChildren.isEmpty) {
+ return [FEmptyNode emptyNode];
+ } else {
+ return [[FChildrenNode alloc] initWithPriority:self.getPriority children:newChildren];
+ }
+ }
+}
+
+- (id<FNode>) updateChild:(FPath *)path withNewChild:(id<FNode>)newChildNode {
+ NSString* front = [path getFront];
+ if(front == nil) {
+ return newChildNode;
+ } else {
+ NSAssert(![front isEqualToString:@".priority"] || path.length == 1, @".priority must be the last token in a path.");
+ id<FNode> newImmediateChild = [[self getImmediateChild:front] updateChild:[path popFront] withNewChild:newChildNode];
+ return [self updateImmediateChild:front withNewChild:newImmediateChild];
+ }
+}
+
+- (BOOL) isEmpty {
+ return [self.children isEmpty];
+}
+
+- (int) numChildren {
+ return [self.children count];
+}
+
+- (id) val {
+ return [self valForExport:NO];
+}
+
+- (id) valForExport:(BOOL)exp {
+ if([self isEmpty]) {
+ return [NSNull null];
+ }
+
+ __block int numKeys = 0;
+ __block NSInteger maxKey = 0;
+ __block BOOL allIntegerKeys = YES;
+
+ NSMutableDictionary* obj = [[NSMutableDictionary alloc] initWithCapacity:[self.children count]];
+ [self enumerateChildrenUsingBlock:^(NSString *key, id<FNode> childNode, BOOL *stop) {
+ [obj setObject:[childNode valForExport:exp] forKey:key];
+
+ numKeys++;
+
+ // If we already found a string key, don't bother with any of this
+ if (!allIntegerKeys) {
+ return;
+ }
+
+ // Treat leading zeroes that are not exactly "0" as strings
+ NSString* firstChar = [key substringWithRange:NSMakeRange(0, 1)];
+ if ([firstChar isEqualToString:@"0"] && [key length] > 1) {
+ allIntegerKeys = NO;
+ } else {
+ NSNumber *keyAsNum = [FUtilities intForString:key];
+ if (keyAsNum != nil) {
+ NSInteger keyAsInt = [keyAsNum integerValue];
+ if (keyAsInt > maxKey) {
+ maxKey = keyAsInt;
+ }
+ } else {
+ allIntegerKeys = NO;
+ }
+ }
+ }];
+
+ if (!exp && allIntegerKeys && maxKey < 2 * numKeys) {
+ // convert to an array
+ NSMutableArray* array = [[NSMutableArray alloc] initWithCapacity:maxKey + 1];
+ for (int i = 0; i <= maxKey; ++i) {
+ NSString* keyString = [NSString stringWithFormat:@"%i", i];
+ id child = obj[keyString];
+ if (child != nil) {
+ [array addObject:child];
+ } else {
+ [array addObject:[NSNull null]];
+ }
+ }
+ return array;
+ } else {
+
+ if(exp && [self getPriority] != nil && !self.getPriority.isEmpty) {
+ obj[kPayloadPriority] = [self.getPriority val];
+ }
+
+ return obj;
+ }
+}
+
+- (NSString *) dataHash {
+ if (self.lazyHash == nil) {
+ NSMutableString *toHash = [[NSMutableString alloc] init];
+
+ if (!self.getPriority.isEmpty) {
+ [toHash appendString:@"priority:"];
+ [FSnapshotUtilities appendHashRepresentationForLeafNode:(FLeafNode *)self.getPriority
+ toString:toHash
+ hashVersion:FDataHashVersionV1];
+ [toHash appendString:@":"];
+ }
+
+ __block BOOL sawPriority = NO;
+ [self enumerateChildrenUsingBlock:^(NSString *key, id<FNode> node, BOOL *stop) {
+ sawPriority = sawPriority || [[node getPriority] isEmpty];
+ *stop = sawPriority;
+ }];
+ if (sawPriority) {
+ NSMutableArray *array = [NSMutableArray array];
+ [self enumerateChildrenUsingBlock:^(NSString *key, id<FNode> node, BOOL *stop) {
+ FNamedNode *namedNode = [[FNamedNode alloc] initWithName:key andNode:node];
+ [array addObject:namedNode];
+ }];
+ [array sortUsingComparator:^NSComparisonResult(FNamedNode *namedNode1, FNamedNode *namedNode2) {
+ return [[FPriorityIndex priorityIndex] compareNamedNode:namedNode1 toNamedNode:namedNode2];
+ }];
+ [array enumerateObjectsUsingBlock:^(FNamedNode *namedNode, NSUInteger idx, BOOL *stop) {
+ NSString *childHash = [namedNode.node dataHash];
+ if (![childHash isEqualToString:@""]) {
+ [toHash appendFormat:@":%@:%@", namedNode.name, childHash];
+ }
+ }];
+ } else {
+ [self enumerateChildrenUsingBlock:^(NSString *key, id<FNode> node, BOOL *stop) {
+ NSString *childHash = [node dataHash];
+ if (![childHash isEqualToString:@""]) {
+ [toHash appendFormat:@":%@:%@", key, childHash];
+ }
+ }];
+ }
+ self.lazyHash = [toHash isEqualToString:@""] ? @"" : [FStringUtilities base64EncodedSha1:toHash];
+ }
+ return self.lazyHash;
+}
+
+- (NSComparisonResult)compare:(id <FNode>)other {
+ // children nodes come last, unless this is actually an empty node, then we come first.
+ if (self.isEmpty) {
+ if (other.isEmpty) {
+ return NSOrderedSame;
+ } else {
+ return NSOrderedAscending;
+ }
+ } else if (other.isLeafNode || other.isEmpty) {
+ return NSOrderedDescending;
+ } else if (other == [FMaxNode maxNode]) {
+ return NSOrderedAscending;
+ } else {
+ // Must be another node with children.
+ return NSOrderedSame;
+ }
+}
+
+- (BOOL)isEqual:(id <FNode>)other {
+ if (other == self) {
+ return YES;
+ } else if (other == nil) {
+ return NO;
+ } else if (other.isLeafNode) {
+ return NO;
+ } else if (self.isEmpty && [other isEmpty]) {
+ // Empty nodes do not have priority
+ return YES;
+ } else {
+ FChildrenNode *otherChildrenNode = other;
+ if (![self.getPriority isEqual:other.getPriority]) {
+ return NO;
+ } else if (self.children.count == otherChildrenNode.children.count) {
+ __block BOOL equal = YES;
+ [self enumerateChildrenUsingBlock:^(NSString *key, id<FNode> node, BOOL *stop) {
+ id<FNode> child = [otherChildrenNode getImmediateChild:key];
+ if (![child isEqual:node]) {
+ equal = NO;
+ *stop = YES;
+ }
+ }];
+ return equal;
+ } else {
+ return NO;
+ }
+ }
+}
+
+- (NSUInteger)hash {
+ __block NSUInteger hashCode = 0;
+ [self enumerateChildrenUsingBlock:^(NSString *key, id<FNode> node, BOOL *stop) {
+ hashCode = 31 * hashCode + key.hash;
+ hashCode = 17 * hashCode + node.hash;
+ }];
+ return 17 * hashCode + self.priorityNode.hash;
+}
+
+- (void) enumerateChildrenAndPriorityUsingBlock:(void (^)(NSString *, id<FNode>, BOOL *))block
+{
+ if ([self.getPriority isEmpty]) {
+ [self enumerateChildrenUsingBlock:block];
+ } else {
+ __block BOOL passedPriorityKey = NO;
+ [self enumerateChildrenUsingBlock:^(NSString *key, id<FNode> node, BOOL *stop) {
+ if (!passedPriorityKey && [FUtilities compareKey:key toKey:@".priority"] == NSOrderedDescending) {
+ passedPriorityKey = YES;
+ BOOL stopAfterPriority = NO;
+ block(@".priority", [self getPriority], &stopAfterPriority);
+ if (stopAfterPriority) return;
+ }
+ block(key, node, stop);
+ }];
+ }
+}
+
+- (void) enumerateChildrenUsingBlock:(void (^)(NSString *, id<FNode>, BOOL *))block
+{
+ [self.children enumerateKeysAndObjectsUsingBlock:block];
+}
+
+- (void) enumerateChildrenReverse:(BOOL)reverse usingBlock:(void (^)(NSString *, id<FNode>, BOOL *))block
+{
+ [self.children enumerateKeysAndObjectsReverse:reverse usingBlock:block];
+}
+
+- (NSEnumerator *)childEnumerator
+{
+ return [[FTransformedEnumerator alloc] initWithEnumerator:self.children.keyEnumerator andTransform:^id(NSString *key) {
+ return [FNamedNode nodeWithName:key node:[self getImmediateChild:key]];
+ }];
+}
+
+- (NSString *) predecessorChildKey:(NSString *)childKey
+{
+ return [self.children getPredecessorKey:childKey];
+}
+
+#pragma mark -
+#pragma mark FChildrenNode specific methods
+
+- (id) childrenGetter:(id)key {
+ return [self.children objectForKey:key];
+}
+
+- (FNamedNode *)firstChild
+{
+ NSString *childKey = self.children.minKey;
+ if (childKey) {
+ return [[FNamedNode alloc] initWithName:childKey andNode:[self getImmediateChild:childKey]];
+ } else {
+ return nil;
+ }
+}
+
+- (FNamedNode *)lastChild
+{
+ NSString *childKey = self.children.maxKey;
+ if (childKey) {
+ return [[FNamedNode alloc] initWithName:childKey andNode:[self getImmediateChild:childKey]];
+ } else {
+ return nil;
+ }
+}
+
+@end
diff --git a/Firebase/Database/Snapshot/FCompoundWrite.h b/Firebase/Database/Snapshot/FCompoundWrite.h
new file mode 100644
index 0000000..c67cfc7
--- /dev/null
+++ b/Firebase/Database/Snapshot/FCompoundWrite.h
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+@class FImmutableTree;
+@protocol FNode;
+@class FPath;
+
+/**
+* This class holds a collection of writes that can be applied to nodes in unison. It abstracts away the logic with
+* dealing with priority writes and multiple nested writes. At any given path, there is only allowed to be one write
+* modifying that path. Any write to an existing path or shadowing an existing path will modify that existing write to
+* reflect the write added.
+*/
+@interface FCompoundWrite : NSObject
+
+- (id) initWithWriteTree:(FImmutableTree *)tree;
+
+/**
+ * Creates a compound write with NSDictionary from path string to object
+ */
++ (FCompoundWrite *) compoundWriteWithValueDictionary:(NSDictionary *)dictionary;
+/**
+ * Creates a compound write with NSDictionary from path string to node
+ */
++ (FCompoundWrite *) compoundWriteWithNodeDictionary:(NSDictionary *)dictionary;
+
++ (FCompoundWrite *) emptyWrite;
+
+- (FCompoundWrite *) addWrite:(id<FNode>)node atPath:(FPath *)path;
+- (FCompoundWrite *) addWrite:(id<FNode>)node atKey:(NSString *)key;
+- (FCompoundWrite *) addCompoundWrite:(FCompoundWrite *)node atPath:(FPath *)path;
+- (FCompoundWrite *) removeWriteAtPath:(FPath *)path;
+- (id<FNode>)rootWrite;
+- (BOOL) hasCompleteWriteAtPath:(FPath *)path;
+- (id<FNode>) completeNodeAtPath:(FPath *)path;
+- (NSArray *) completeChildren;
+- (NSDictionary *)childCompoundWrites;
+- (FCompoundWrite *) childCompoundWriteAtPath:(FPath *)path;
+- (id<FNode>) applyToNode:(id<FNode>)node;
+- (void)enumerateWrites:(void (^)(FPath *path, id<FNode>node, BOOL *stop))block;
+
+- (NSDictionary *)valForExport:(BOOL)exportFormat;
+
+- (BOOL) isEmpty;
+
+@end
diff --git a/Firebase/Database/Snapshot/FCompoundWrite.m b/Firebase/Database/Snapshot/FCompoundWrite.m
new file mode 100644
index 0000000..8887095
--- /dev/null
+++ b/Firebase/Database/Snapshot/FCompoundWrite.m
@@ -0,0 +1,257 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FCompoundWrite.h"
+#import "FImmutableTree.h"
+#import "FNode.h"
+#import "FPath.h"
+#import "FNamedNode.h"
+#import "FSnapshotUtilities.h"
+
+@interface FCompoundWrite ()
+@property (nonatomic, strong) FImmutableTree *writeTree;
+@end
+
+@implementation FCompoundWrite
+
+- (id) initWithWriteTree:(FImmutableTree *)tree {
+ self = [super init];
+ if (self) {
+ self.writeTree = tree;
+ }
+ return self;
+}
+
++ (FCompoundWrite *)compoundWriteWithValueDictionary:(NSDictionary *)dictionary {
+ __block FImmutableTree *writeTree = [FImmutableTree empty];
+ [dictionary enumerateKeysAndObjectsUsingBlock:^(NSString *pathString, id value, BOOL *stop) {
+ id<FNode> node = [FSnapshotUtilities nodeFrom:value];
+ FImmutableTree *tree = [[FImmutableTree alloc] initWithValue:node];
+ writeTree = [writeTree setTree:tree atPath:[[FPath alloc] initWith:pathString]];
+ }];
+ return [[FCompoundWrite alloc] initWithWriteTree:writeTree];
+}
+
++ (FCompoundWrite *)compoundWriteWithNodeDictionary:(NSDictionary *)dictionary {
+ __block FImmutableTree *writeTree = [FImmutableTree empty];
+ [dictionary enumerateKeysAndObjectsUsingBlock:^(NSString *pathString, id node, BOOL *stop) {
+ FImmutableTree *tree = [[FImmutableTree alloc] initWithValue:node];
+ writeTree = [writeTree setTree:tree atPath:[[FPath alloc] initWith:pathString]];
+ }];
+ return [[FCompoundWrite alloc] initWithWriteTree:writeTree];
+}
+
++ (FCompoundWrite *) emptyWrite {
+ static dispatch_once_t pred = 0;
+ static FCompoundWrite *empty = nil;
+ dispatch_once(&pred, ^{
+ empty = [[FCompoundWrite alloc] initWithWriteTree:[[FImmutableTree alloc] initWithValue:nil]];
+ });
+ return empty;
+}
+
+- (FCompoundWrite *) addWrite:(id<FNode>)node atPath:(FPath *)path {
+ if (path.isEmpty) {
+ return [[FCompoundWrite alloc] initWithWriteTree:[[FImmutableTree alloc] initWithValue:node]];
+ } else {
+ FTuplePathValue *rootMost = [self.writeTree findRootMostValueAndPath:path];
+ if (rootMost != nil) {
+ FPath *relativePath = [FPath relativePathFrom:rootMost.path to:path];
+ id<FNode> value = [rootMost.value updateChild:relativePath withNewChild:node];
+ return [[FCompoundWrite alloc] initWithWriteTree:[self.writeTree setValue:value atPath:rootMost.path]];
+ } else {
+ FImmutableTree *subtree = [[FImmutableTree alloc] initWithValue:node];
+ FImmutableTree *newWriteTree = [self.writeTree setTree:subtree atPath:path];
+ return [[FCompoundWrite alloc] initWithWriteTree:newWriteTree];
+ }
+ }
+}
+
+- (FCompoundWrite *) addWrite:(id<FNode>)node atKey:(NSString *)key {
+ return [self addWrite:node atPath:[[FPath alloc] initWith:key]];
+}
+
+- (FCompoundWrite *) addCompoundWrite:(FCompoundWrite *)compoundWrite atPath:(FPath *)path {
+ __block FCompoundWrite *newWrite = self;
+ [compoundWrite.writeTree forEach:^(FPath *childPath, id<FNode> value) {
+ newWrite = [newWrite addWrite:value atPath:[path child:childPath]];
+ }];
+ return newWrite;
+}
+
+/**
+* Will remove a write at the given path and deeper paths. This will <em>not</em> modify a write at a higher location,
+* which must be removed by calling this method with that path.
+* @param path The path at which a write and all deeper writes should be removed.
+* @return The new FWriteCompound with the removed path.
+*/
+- (FCompoundWrite *) removeWriteAtPath:(FPath *)path {
+ if (path.isEmpty) {
+ return [FCompoundWrite emptyWrite];
+ } else {
+ FImmutableTree *newWriteTree = [self.writeTree setTree:[FImmutableTree empty] atPath:path];
+ return [[FCompoundWrite alloc] initWithWriteTree:newWriteTree];
+ }
+}
+
+/**
+* Returns whether this FCompoundWrite will fully overwrite a node at a given location and can therefore be considered
+* "complete".
+* @param path The path to check for
+* @return Whether there is a complete write at that path.
+*/
+- (BOOL) hasCompleteWriteAtPath:(FPath *)path {
+ return [self completeNodeAtPath:path] != nil;
+}
+
+/**
+* Returns a node for a path if and only if the node is a "complete" overwrite at that path. This will not aggregate
+* writes from depeer paths, but will return child nodes from a more shallow path.
+* @param path The path to get a complete write
+* @return The node if complete at that path, or nil otherwise.
+*/
+- (id<FNode>) completeNodeAtPath:(FPath *)path {
+ FTuplePathValue *rootMost = [self.writeTree findRootMostValueAndPath:path];
+ if (rootMost != nil) {
+ FPath *relativePath = [FPath relativePathFrom:rootMost.path to:path];
+ return [rootMost.value getChild:relativePath];
+ } else {
+ return nil;
+ }
+}
+
+// TODO: change into traversal method...
+- (NSArray *) completeChildren {
+ NSMutableArray *children = [[NSMutableArray alloc] init];
+ if (self.writeTree.value != nil) {
+ id<FNode> node = self.writeTree.value;
+ [node enumerateChildrenUsingBlock:^(NSString *key, id<FNode> node, BOOL *stop) {
+ [children addObject:[[FNamedNode alloc] initWithName:key andNode:node]];
+ }];
+ } else {
+ [self.writeTree.children enumerateKeysAndObjectsUsingBlock:^(NSString *childKey, FImmutableTree *childTree, BOOL *stop) {
+ if (childTree.value != nil) {
+ [children addObject:[[FNamedNode alloc] initWithName:childKey andNode:childTree.value]];
+ }
+ }];
+ }
+ return children;
+}
+
+
+// TODO: change into enumarate method
+- (NSDictionary *)childCompoundWrites {
+ NSMutableDictionary *dict = [NSMutableDictionary dictionary];
+ [self.writeTree.children enumerateKeysAndObjectsUsingBlock:^(NSString *key, FImmutableTree *childWrite, BOOL *stop) {
+ dict[key] = [[FCompoundWrite alloc] initWithWriteTree:childWrite];
+ }];
+ return dict;
+}
+
+- (FCompoundWrite *) childCompoundWriteAtPath:(FPath *)path {
+ if (path.isEmpty) {
+ return self;
+ } else {
+ id<FNode> shadowingNode = [self completeNodeAtPath:path];
+ if (shadowingNode != nil) {
+ return [[FCompoundWrite alloc] initWithWriteTree:[[FImmutableTree alloc] initWithValue:shadowingNode]];
+ } else {
+ return [[FCompoundWrite alloc] initWithWriteTree:[self.writeTree subtreeAtPath:path]];
+ }
+ }
+}
+
+- (id<FNode>) applySubtreeWrite:(FImmutableTree *)subtreeWrite atPath:(FPath *)relativePath toNode:(id<FNode>)node {
+ if (subtreeWrite.value != nil) {
+ // Since a write there is always a leaf, we're done here.
+ return [node updateChild:relativePath withNewChild:subtreeWrite.value];
+ } else {
+ __block id<FNode> priorityWrite = nil;
+ __block id<FNode> blockNode = node;
+ [subtreeWrite.children enumerateKeysAndObjectsUsingBlock:^(NSString *childKey, FImmutableTree *childTree, BOOL *stop) {
+ if ([childKey isEqualToString:@".priority"]) {
+ // Apply priorities at the end so we don't update priorities for either empty nodes or forget to apply
+ // priorities to empty nodes that are later filled.
+ NSAssert(childTree.value != nil, @"Priority writes must always be leaf nodes");
+ priorityWrite = childTree.value;
+ } else {
+ blockNode = [self applySubtreeWrite:childTree atPath:[relativePath childFromString:childKey] toNode:blockNode];
+ }
+ }];
+ // If there was a priority write, we only apply it if the node is not empty
+ if (![blockNode getChild:relativePath].isEmpty && priorityWrite != nil) {
+ blockNode = [blockNode updateChild:[relativePath childFromString:@".priority"] withNewChild:priorityWrite];
+ }
+ return blockNode;
+ }
+}
+
+- (void)enumerateWrites:(void (^)(FPath *, id<FNode>, BOOL *))block {
+ __block BOOL stop = NO;
+ // TODO: add stop to tree iterator...
+ [self.writeTree forEach:^(FPath *path, id value) {
+ if (!stop) {
+ block(path, value, &stop);
+ }
+ }];
+}
+
+/**
+* Applies this FCompoundWrite to a node. The node is returned with all writes from this FCompoundWrite applied to the node.
+* @param node The node to apply this FCompoundWrite to
+* @return The node with all writes applied
+*/
+- (id<FNode>) applyToNode:(id<FNode>)node {
+ return [self applySubtreeWrite:self.writeTree atPath:[FPath empty] toNode:node];
+}
+
+/**
+* Return true if this CompoundWrite is empty and therefore does not modify any nodes.
+* @return Whether this CompoundWrite is empty
+*/
+- (BOOL) isEmpty {
+ return self.writeTree.isEmpty;
+}
+
+- (id<FNode>) rootWrite {
+ return self.writeTree.value;
+}
+
+- (BOOL)isEqual:(id)object {
+ if (![object isKindOfClass:[FCompoundWrite class]]) {
+ return NO;
+ }
+ FCompoundWrite *other = (FCompoundWrite *)object;
+ return [[self valForExport:YES] isEqualToDictionary:[other valForExport:YES]];
+}
+
+- (NSUInteger)hash {
+ return [[self valForExport:YES] hash];
+}
+
+- (NSDictionary *)valForExport:(BOOL)exportFormat {
+ NSMutableDictionary *dictionary = [NSMutableDictionary dictionary];
+ [self.writeTree forEach:^(FPath *path, id<FNode> value) {
+ dictionary[path.wireFormat] = [value valForExport:exportFormat];
+ }];
+ return dictionary;
+}
+
+- (NSString *)description {
+ return [[self valForExport:YES] description];
+}
+
+@end
diff --git a/Firebase/Database/Snapshot/FEmptyNode.h b/Firebase/Database/Snapshot/FEmptyNode.h
new file mode 100644
index 0000000..ab404c2
--- /dev/null
+++ b/Firebase/Database/Snapshot/FEmptyNode.h
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+#import "FNode.h"
+
+@interface FEmptyNode : NSObject
+
++ (id<FNode>) emptyNode;
+
+@end
diff --git a/Firebase/Database/Snapshot/FEmptyNode.m b/Firebase/Database/Snapshot/FEmptyNode.m
new file mode 100644
index 0000000..dd2d9ea
--- /dev/null
+++ b/Firebase/Database/Snapshot/FEmptyNode.m
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FEmptyNode.h"
+#import "FChildrenNode.h"
+
+@implementation FEmptyNode
+
++ (id<FNode>) emptyNode {
+ static FChildrenNode* empty = nil;
+ if (empty == nil) {
+ empty = [[FChildrenNode alloc] init];
+ }
+ return empty;
+}
+@end
diff --git a/Firebase/Database/Snapshot/FIndexedNode.h b/Firebase/Database/Snapshot/FIndexedNode.h
new file mode 100644
index 0000000..fd2db37
--- /dev/null
+++ b/Firebase/Database/Snapshot/FIndexedNode.h
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+#import <Foundation/Foundation.h>
+
+#import "FNode.h"
+#import "FIndex.h"
+#import "FNamedNode.h"
+
+/**
+ * Represents a node together with an index. The index and node are updated in unison. In the case where the index
+ * does not affect the ordering (i.e. the ordering is identical to the key ordering) this class uses a fallback index
+ * to save memory. Everything operating on the index must special case the fallback index.
+ */
+@interface FIndexedNode : NSObject
+
+@property (nonatomic, strong, readonly) id<FNode> node;
+
++ (FIndexedNode *)indexedNodeWithNode:(id<FNode>)node;
++ (FIndexedNode *)indexedNodeWithNode:(id<FNode>)node index:(id<FIndex>)index;
+
+- (BOOL)hasIndex:(id<FIndex>)index;
+- (FIndexedNode *)updateChild:(NSString *)key withNewChild:(id<FNode>)newChildNode;
+- (FIndexedNode *)updatePriority:(id<FNode>)priority;
+
+- (FNamedNode *)firstChild;
+- (FNamedNode *)lastChild;
+
+- (NSString *)predecessorForChildKey:(NSString *)childKey childNode:(id<FNode>)childNode index:(id<FIndex>)index;
+
+- (void)enumerateChildrenReverse:(BOOL)reverse usingBlock:(void (^)(NSString *key, id<FNode> node, BOOL *stop))block;
+
+- (NSEnumerator *)childEnumerator;
+
+@end
diff --git a/Firebase/Database/Snapshot/FIndexedNode.m b/Firebase/Database/Snapshot/FIndexedNode.m
new file mode 100644
index 0000000..e874dcf
--- /dev/null
+++ b/Firebase/Database/Snapshot/FIndexedNode.m
@@ -0,0 +1,202 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIndexedNode.h"
+
+#import "FImmutableSortedSet.h"
+#import "FIndex.h"
+#import "FPriorityIndex.h"
+#import "FKeyIndex.h"
+#import "FChildrenNode.h"
+
+static FImmutableSortedSet *FALLBACK_INDEX;
+
+@interface FIndexedNode ()
+
+@property (nonatomic, strong) id<FNode> node;
+/**
+ * The indexed set is initialized lazily to prevent creation when it is not needed
+ */
+@property (nonatomic, strong) FImmutableSortedSet *indexed;
+@property (nonatomic, strong) id<FIndex> index;
+
+@end
+
+@implementation FIndexedNode
+
++ (FImmutableSortedSet *)fallbackIndex {
+ static FImmutableSortedSet *fallbackIndex;
+ static dispatch_once_t once;
+ dispatch_once(&once, ^{
+ fallbackIndex = [[FImmutableSortedSet alloc] init];
+ });
+ return fallbackIndex;
+}
+
++ (FIndexedNode *)indexedNodeWithNode:(id<FNode>)node
+{
+ return [[FIndexedNode alloc] initWithNode:node index:[FPriorityIndex priorityIndex]];
+}
+
++ (FIndexedNode *)indexedNodeWithNode:(id<FNode>)node index:(id<FIndex>)index
+{
+ return [[FIndexedNode alloc] initWithNode:node index:index];
+}
+
+- (id)initWithNode:(id<FNode>)node index:(id<FIndex>)index
+{
+ // Initialize indexed lazily
+ return [self initWithNode:node index:index indexed:nil];
+}
+
+- (id)initWithNode:(id<FNode>)node index:(id<FIndex>)index indexed:(FImmutableSortedSet *)indexed
+{
+ self = [super init];
+ if (self != nil) {
+ self->_node = node;
+ self->_index = index;
+ self->_indexed = indexed;
+ }
+ return self;
+}
+
+- (void)ensureIndexed
+{
+ if (!self.indexed) {
+ if ([self.index isEqual:[FKeyIndex keyIndex]]) {
+ self.indexed = [FIndexedNode fallbackIndex];
+ } else {
+ __block BOOL sawChild;
+ [self.node enumerateChildrenUsingBlock:^(NSString *key, id<FNode> node, BOOL *stop) {
+ sawChild = sawChild || [self.index isDefinedOn:node];
+ *stop = sawChild;
+ }];
+ if (sawChild) {
+ NSMutableDictionary *dict = [NSMutableDictionary dictionary];
+ [self.node enumerateChildrenUsingBlock:^(NSString *key, id<FNode> node, BOOL *stop) {
+ FNamedNode *namedNode = [[FNamedNode alloc] initWithName:key andNode:node];
+ dict[namedNode] = [NSNull null];
+ }];
+ // Make sure to assign index here, because the comparator will be retained and using self will cause a
+ // cycle
+ id<FIndex> index = self.index;
+ self.indexed = [FImmutableSortedSet setWithKeysFromDictionary:dict
+ comparator:^NSComparisonResult(FNamedNode *namedNode1, FNamedNode *namedNode2) {
+ return [index compareNamedNode:namedNode1 toNamedNode:namedNode2];
+ }];
+ } else {
+ self.indexed = [FIndexedNode fallbackIndex];
+ }
+ }
+ }
+}
+
+- (BOOL)hasIndex:(id<FIndex>)index
+{
+ return [self.index isEqual:index];
+}
+
+- (FIndexedNode *)updateChild:(NSString *)key withNewChild:(id<FNode>)newChildNode
+{
+ id<FNode> newNode = [self.node updateImmediateChild:key withNewChild:newChildNode];
+ if (self.indexed == [FIndexedNode fallbackIndex] && ![self.index isDefinedOn:newChildNode]) {
+ // doesn't affect the index, no need to create an index
+ return [[FIndexedNode alloc] initWithNode:newNode index:self.index indexed:[FIndexedNode fallbackIndex]];
+ } else if (!self.indexed || self.indexed == [FIndexedNode fallbackIndex]) {
+ // No need to index yet, index lazily
+ return [[FIndexedNode alloc] initWithNode:newNode index:self.index];
+ } else {
+ id<FNode> oldChild = [self.node getImmediateChild:key];
+ FImmutableSortedSet *newIndexed = [self.indexed removeObject:[FNamedNode nodeWithName:key node:oldChild]];
+ if (![newChildNode isEmpty]) {
+ newIndexed = [newIndexed addObject:[FNamedNode nodeWithName:key node:newChildNode]];
+ }
+ return [[FIndexedNode alloc] initWithNode:newNode index:self.index indexed:newIndexed];
+ }
+}
+
+- (FIndexedNode *)updatePriority:(id<FNode>)priority
+{
+ return [[FIndexedNode alloc] initWithNode:[self.node updatePriority:priority]
+ index:self.index
+ indexed:self.indexed];
+}
+
+- (FNamedNode *)firstChild
+{
+ if (![self.node isKindOfClass:[FChildrenNode class]]) {
+ return nil;
+ } else {
+ [self ensureIndexed];
+ if (self.indexed == [FIndexedNode fallbackIndex]) {
+ return [((FChildrenNode *)self.node) firstChild];
+ } else {
+ return self.indexed.firstObject;
+ }
+ }
+}
+
+- (FNamedNode *)lastChild
+{
+ if (![self.node isKindOfClass:[FChildrenNode class]]) {
+ return nil;
+ } else {
+ [self ensureIndexed];
+ if (self.indexed == [FIndexedNode fallbackIndex]) {
+ return [((FChildrenNode *)self.node) lastChild];
+ } else {
+ return self.indexed.lastObject;
+ }
+ }
+}
+
+- (NSString *)predecessorForChildKey:(NSString *)childKey childNode:(id<FNode>)childNode index:(id<FIndex>)index
+{
+ if (![self.index isEqual:index]) {
+ [NSException raise:NSInvalidArgumentException format:@"Index not available in IndexedNode!"];
+ }
+ [self ensureIndexed];
+ if (self.indexed == [FIndexedNode fallbackIndex]) {
+ return [self.node predecessorChildKey:childKey];
+ } else {
+ FNamedNode *node = [self.indexed predecessorEntry:[FNamedNode nodeWithName:childKey node:childNode]];
+ return node.name;
+ }
+}
+
+- (void)enumerateChildrenReverse:(BOOL)reverse usingBlock:(void (^)(NSString *, id<FNode>, BOOL *))block
+{
+ [self ensureIndexed];
+ if (self.indexed == [FIndexedNode fallbackIndex]) {
+ [self.node enumerateChildrenReverse:reverse usingBlock:block];
+ } else {
+ [self.indexed enumerateObjectsReverse:reverse usingBlock:^(FNamedNode *namedNode, BOOL *stop) {
+ block(namedNode.name, namedNode.node, stop);
+ }];
+ }
+}
+
+- (NSEnumerator *)childEnumerator
+{
+ [self ensureIndexed];
+ if (self.indexed == [FIndexedNode fallbackIndex]) {
+ return [self.node childEnumerator];
+ } else {
+ return [self.indexed objectEnumerator];
+ }
+}
+
+@end
diff --git a/Firebase/Database/Snapshot/FLeafNode.h b/Firebase/Database/Snapshot/FLeafNode.h
new file mode 100644
index 0000000..15e0132
--- /dev/null
+++ b/Firebase/Database/Snapshot/FLeafNode.h
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+#import "FNode.h"
+
+
+@interface FLeafNode : NSObject <FNode>
+
+- (id)initWithValue:(id)aValue;
+- (id)initWithValue:(id)aValue withPriority:(id<FNode>)aPriority;
+
+@property (nonatomic, strong) id value;
+
+@end
diff --git a/Firebase/Database/Snapshot/FLeafNode.m b/Firebase/Database/Snapshot/FLeafNode.m
new file mode 100644
index 0000000..a26e057
--- /dev/null
+++ b/Firebase/Database/Snapshot/FLeafNode.m
@@ -0,0 +1,250 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FLeafNode.h"
+#import "FEmptyNode.h"
+#import "FChildrenNode.h"
+#import "FConstants.h"
+#import "FImmutableSortedDictionary.h"
+#import "FUtilities.h"
+#import "FStringUtilities.h"
+#import "FSnapshotUtilities.h"
+
+@interface FLeafNode ()
+@property (nonatomic, strong) id<FNode> priorityNode;
+@property (nonatomic, strong) NSString *lazyHash;
+
+@end
+
+@implementation FLeafNode
+
+@synthesize value;
+@synthesize priorityNode;
+
+- (id)initWithValue:(id)aValue {
+ self = [super init];
+ if (self) {
+ self.value = aValue;
+ self.priorityNode = [FEmptyNode emptyNode];
+ }
+ return self;
+}
+
+- (id)initWithValue:(id)aValue withPriority:(id<FNode>)aPriority {
+ self = [super init];
+ if (self) {
+ self.value = aValue;
+ [FSnapshotUtilities validatePriorityNode:aPriority];
+ self.priorityNode = aPriority;
+ }
+ return self;
+}
+
+#pragma mark -
+#pragma mark FNode methods
+
+- (BOOL) isLeafNode {
+ return YES;
+}
+
+- (id<FNode>) getPriority {
+ return self.priorityNode;
+}
+
+- (id<FNode>) updatePriority:(id<FNode>)aPriority {
+ return [[FLeafNode alloc] initWithValue:self.value withPriority:aPriority];
+}
+
+- (id<FNode>) getImmediateChild:(NSString *) childName {
+ if ([childName isEqualToString:@".priority"]) {
+ return self.priorityNode;
+ } else {
+ return [FEmptyNode emptyNode];
+ }
+}
+
+- (id<FNode>) getChild:(FPath *)path {
+ if (path.getFront == nil) {
+ return self;
+ } else if ([[path getFront] isEqualToString:@".priority"]) {
+ return [self getPriority];
+ } else {
+ return [FEmptyNode emptyNode];
+ }
+}
+
+- (BOOL)hasChild:(NSString *)childName {
+ return [childName isEqualToString:@".priority"] && ![self getPriority].isEmpty;
+}
+
+
+- (NSString *)predecessorChildKey:(NSString *)childKey
+{
+ return nil;
+}
+
+- (id<FNode>) updateImmediateChild:(NSString *)childName withNewChild:(id<FNode>)newChildNode {
+ if ([childName isEqualToString:@".priority"]) {
+ return [self updatePriority:newChildNode];
+ } else if (newChildNode.isEmpty) {
+ return self;
+ } else {
+ FChildrenNode* childrenNode = [[FChildrenNode alloc] init];
+ childrenNode = [childrenNode updateImmediateChild:childName withNewChild:newChildNode];
+ childrenNode = [childrenNode updatePriority:self.priorityNode];
+ return childrenNode;
+ }
+}
+
+- (id<FNode>) updateChild:(FPath *)path withNewChild:(id<FNode>)newChildNode {
+ NSString* front = [path getFront];
+ if(front == nil) {
+ return newChildNode;
+ } else if (newChildNode.isEmpty && ![front isEqualToString:@".priority"]) {
+ return self;
+ } else {
+ NSAssert(![front isEqualToString:@".priority"] || path.length == 1, @".priority must be the last token in a path.");
+ return [self updateImmediateChild:front withNewChild:
+ [[FEmptyNode emptyNode] updateChild:[path popFront] withNewChild:newChildNode]];
+ }
+}
+
+- (id) val {
+ return [self valForExport:NO];
+}
+
+- (id) valForExport:(BOOL)exp {
+ if(exp && !self.getPriority.isEmpty) {
+ return @{kPayloadValue : self.value,
+ kPayloadPriority : [[self getPriority] val]};
+ }
+ else {
+ return self.value;
+ }
+}
+
+- (BOOL)isEqual:(id <FNode>)other {
+ if(other == self) {
+ return YES;
+ } else if (other.isLeafNode) {
+ FLeafNode *otherLeaf = other;
+ if ([FUtilities getJavascriptType:self.value] != [FUtilities getJavascriptType:otherLeaf.value]) {
+ return NO;
+ }
+ return [otherLeaf.value isEqual:self.value] && [otherLeaf.priorityNode isEqual:self.priorityNode];
+ } else {
+ return NO;
+ }
+}
+
+- (NSUInteger)hash {
+ return [self.value hash] * 17 + self.priorityNode.hash;
+}
+
+- (id <FNode>)withIndex:(id <FIndex>)index {
+ return self;
+}
+
+- (BOOL)isIndexed:(id <FIndex>)index {
+ return YES;
+}
+
+- (BOOL) isEmpty {
+ return NO;
+}
+
+- (int) numChildren {
+ return 0;
+}
+
+- (void) enumerateChildrenUsingBlock:(void (^)(NSString *, id<FNode>, BOOL *))block
+{
+ // Nothing to iterate over
+}
+
+- (void) enumerateChildrenReverse:(BOOL)reverse usingBlock:(void (^)(NSString *, id<FNode>, BOOL *))block
+{
+ // Nothing to iterate over
+}
+
+- (NSEnumerator *)childEnumerator
+{
+ // Nothing to iterate over
+ return [@[] objectEnumerator];
+}
+
+- (NSString *) dataHash {
+ if (self.lazyHash == nil) {
+ NSMutableString *toHash = [[NSMutableString alloc] init];
+ [FSnapshotUtilities appendHashRepresentationForLeafNode:self toString:toHash hashVersion:FDataHashVersionV1];
+
+ self.lazyHash = [FStringUtilities base64EncodedSha1:toHash];
+ }
+ return self.lazyHash;
+}
+
+- (NSComparisonResult)compare:(id <FNode>)other {
+ if (other == [FEmptyNode emptyNode]) {
+ return NSOrderedDescending;
+ } else if ([other isKindOfClass:[FChildrenNode class]]) {
+ return NSOrderedAscending;
+ } else {
+ NSAssert(other.isLeafNode, @"Compared against unknown type of node.");
+ return [self compareToLeafNode:(FLeafNode*)other];
+ }
+}
+
++ (NSArray*) valueTypeOrder {
+ static NSArray* valueOrder = nil;
+ static dispatch_once_t once;
+ dispatch_once(&once, ^{
+ valueOrder = @[kJavaScriptObject, kJavaScriptBoolean, kJavaScriptNumber, kJavaScriptString];
+ });
+ return valueOrder;
+}
+
+- (NSComparisonResult) compareToLeafNode:(FLeafNode*)other {
+ NSString* thisLeafType = [FUtilities getJavascriptType:self.value];
+ NSString* otherLeafType = [FUtilities getJavascriptType:other.value];
+ NSUInteger thisIndex = [[FLeafNode valueTypeOrder] indexOfObject:thisLeafType];
+ NSUInteger otherIndex = [[FLeafNode valueTypeOrder] indexOfObject:otherLeafType];
+ assert(thisIndex >= 0 && otherIndex >= 0);
+ if (otherIndex == thisIndex) {
+ // Same type. Compare values.
+ if (thisLeafType == kJavaScriptObject) {
+ // Deferred value nodes are all equal, but we should also never get to this point...
+ return NSOrderedSame;
+ } else if (thisLeafType == kJavaScriptString) {
+ return [self.value compare:other.value options:NSLiteralSearch];
+ } else {
+ return [self.value compare:other.value];
+ }
+ } else {
+ return thisIndex > otherIndex ? NSOrderedDescending : NSOrderedAscending;
+ }
+}
+
+- (NSString *) description {
+ return [[self valForExport:YES] description];
+}
+
+- (void) forEachChildDo:(fbt_bool_nsstring_node)action {
+ // There are no children, so there is nothing to do.
+ return;
+}
+
+
+@end
diff --git a/Firebase/Database/Snapshot/FNode.h b/Firebase/Database/Snapshot/FNode.h
new file mode 100644
index 0000000..1316756
--- /dev/null
+++ b/Firebase/Database/Snapshot/FNode.h
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+#import "FPath.h"
+#import "FTypedefs_Private.h"
+
+@protocol FIndex;
+
+@protocol FNode <NSObject>
+
+- (BOOL) isLeafNode;
+- (id<FNode>) getPriority;
+- (id<FNode>) updatePriority:(id<FNode>)priority;
+- (id<FNode>) getImmediateChild:(NSString *)childKey;
+- (id<FNode>) getChild:(FPath *)path;
+- (NSString *) predecessorChildKey:(NSString *)childKey;
+- (id<FNode>) updateImmediateChild:(NSString *)childKey withNewChild:(id<FNode>)newChildNode;
+- (id<FNode>) updateChild:(FPath *)path withNewChild:(id<FNode>)newChildNode;
+- (BOOL) hasChild:(NSString*)childKey;
+- (BOOL) isEmpty;
+- (int) numChildren;
+- (id) val;
+- (id) valForExport:(BOOL)exp;
+- (NSString *) dataHash;
+- (NSComparisonResult) compare:(id<FNode>)other;
+- (BOOL) isEqual:(id<FNode>)other;
+- (void)enumerateChildrenUsingBlock:(void (^)(NSString *key, id<FNode> node, BOOL *stop))block;
+- (void)enumerateChildrenReverse:(BOOL)reverse usingBlock:(void (^)(NSString *key, id<FNode> node, BOOL *stop))block;
+
+- (NSEnumerator *)childEnumerator;
+
+@end
diff --git a/Firebase/Database/Snapshot/FSnapshotUtilities.h b/Firebase/Database/Snapshot/FSnapshotUtilities.h
new file mode 100644
index 0000000..2a28788
--- /dev/null
+++ b/Firebase/Database/Snapshot/FSnapshotUtilities.h
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+#import "FNode.h"
+
+@class FImmutableSortedDictionary;
+@class FCompoundWrite;
+@class FLeafNode;
+@protocol FNode;
+
+typedef NS_ENUM(NSInteger, FDataHashVersion) {
+ FDataHashVersionV1,
+ FDataHashVersionV2,
+};
+
+@interface FSnapshotUtilities : NSObject
+
++ (id<FNode>) nodeFrom:(id)val;
++ (id<FNode>) nodeFrom:(id)val priority:(id)priority;
++ (id<FNode>) nodeFrom:(id)val withValidationFrom:(NSString *)fn;
++ (id<FNode>) nodeFrom:(id)val priority:(id)priority withValidationFrom:(NSString *)fn;
++ (FCompoundWrite *) compoundWriteFromDictionary:(NSDictionary *)values withValidationFrom:(NSString *)fn;
++ (void) validatePriorityNode:(id<FNode>)priorityNode;
++ (void)appendHashRepresentationForLeafNode:(FLeafNode *)val
+ toString:(NSMutableString *)string
+ hashVersion:(FDataHashVersion)hashVersion;
++ (void)appendHashV2RepresentationForString:(NSString *)string toString:(NSMutableString *)mutableString;
+
++ (NSUInteger)estimateSerializedNodeSize:(id<FNode>)node;
+
+@end
diff --git a/Firebase/Database/Snapshot/FSnapshotUtilities.m b/Firebase/Database/Snapshot/FSnapshotUtilities.m
new file mode 100644
index 0000000..1b83430
--- /dev/null
+++ b/Firebase/Database/Snapshot/FSnapshotUtilities.m
@@ -0,0 +1,301 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FSnapshotUtilities.h"
+#import "FEmptyNode.h"
+#import "FLeafNode.h"
+#import "FConstants.h"
+#import "FUtilities.h"
+#import "FChildrenNode.h"
+#import "FLLRBValueNode.h"
+#import "FValidation.h"
+#import "FMaxNode.h"
+#import "FNamedNode.h"
+#import "FCompoundWrite.h"
+
+@implementation FSnapshotUtilities
+
++ (id<FNode>) nodeFrom:(id)val {
+ return [FSnapshotUtilities nodeFrom:val priority:nil];
+}
+
++ (id<FNode>) nodeFrom:(id)val priority:(id)priority {
+ return [FSnapshotUtilities nodeFrom:val priority:priority withValidationFrom:@"nodeFrom:priority:"];
+}
+
++ (id<FNode>) nodeFrom:(id)val withValidationFrom:(NSString *)fn {
+ return [FSnapshotUtilities nodeFrom:val priority:nil withValidationFrom:fn];
+}
+
++ (id<FNode>) nodeFrom:(id)val priority:(id)priority withValidationFrom:(NSString *)fn {
+ return [FSnapshotUtilities nodeFrom:val priority:priority withValidationFrom:fn atDepth:0 path:[[NSMutableArray alloc] init]];
+}
+
++ (id<FNode>) nodeFrom:(id)val priority:(id)aPriority withValidationFrom:(NSString *)fn atDepth:(int)depth path:(NSMutableArray *)path {
+ @autoreleasepool {
+ return [FSnapshotUtilities internalNodeFrom:val priority:aPriority withValidationFrom:fn atDepth:depth path:path];
+ }
+}
+
++ (id<FNode>) internalNodeFrom:(id)val priority:(id)aPriority withValidationFrom:(NSString *)fn atDepth:(int)depth path:(NSMutableArray *)path {
+
+
+ if (depth > kFirebaseMaxObjectDepth) {
+ NSRange range;
+ range.location = 0;
+ range.length = 100;
+ NSString* pathString = [[path subarrayWithRange:range] componentsJoinedByString:@"."];
+ @throw [[NSException alloc] initWithName:@"InvalidFirebaseData" reason:[NSString stringWithFormat:@"(%@) Max object depth exceeded: %@...", fn, pathString] userInfo:nil];
+ }
+
+ if (val == nil || val == [NSNull null]) {
+ // Null is a valid type to store
+ return [FEmptyNode emptyNode];
+ }
+
+ [FValidation validateFrom:fn isValidPriorityValue:aPriority withPath:path];
+ id<FNode> priority = [FSnapshotUtilities nodeFrom:aPriority];
+
+ id value = val;
+ BOOL isLeafNode = NO;
+
+ if([value isKindOfClass:[NSDictionary class]]) {
+ NSDictionary* dict = val;
+ if(dict[kPayloadPriority] != nil) {
+ id rawPriority = [dict objectForKey:kPayloadPriority];
+ [FValidation validateFrom:fn isValidPriorityValue:rawPriority withPath:path];
+ priority = [FSnapshotUtilities nodeFrom:rawPriority];
+ }
+
+ if(dict[kPayloadValue] != nil) {
+ value = [dict objectForKey:kPayloadValue];
+ if ([FValidation validateFrom:fn isValidLeafValue:value withPath:path]) {
+ isLeafNode = YES;
+ } else {
+ @throw [[NSException alloc]
+ initWithName:@"InvalidLeafValueType"
+ reason:[NSString stringWithFormat:@"(%@) Invalid data type used with .value. Can only use "
+ "NSString and NSNumber or be null. Found %@ instead.",
+ fn, [[value class] description]] userInfo:nil];
+ }
+ }
+ }
+
+ if([FValidation validateFrom:fn isValidLeafValue:value withPath:path]) {
+ isLeafNode = YES;
+ }
+
+ if (isLeafNode) {
+ return [[FLeafNode alloc] initWithValue:value withPriority:priority];
+ }
+
+ // Unlike with JS, we have to handle the dictionary and array cases separately.
+ if ([value isKindOfClass:[NSDictionary class]]) {
+ NSDictionary* dval = (NSDictionary *)value;
+ NSMutableDictionary *children = [NSMutableDictionary dictionaryWithCapacity:dval.count];
+
+ // Avoid creating a million newPaths by appending to old one
+ for (id keyId in dval) {
+ [FValidation validateFrom:fn validDictionaryKey:keyId withPath:path];
+ NSString* key = (NSString*)keyId;
+
+ if (![key hasPrefix:kPayloadMetadataPrefix]) {
+ [path addObject:key];
+ id<FNode> childNode = [FSnapshotUtilities nodeFrom:dval[key] priority:nil withValidationFrom:fn atDepth:depth + 1 path:path];
+ [path removeLastObject];
+
+ if (![childNode isEmpty]) {
+ children[key] = childNode;
+ }
+ }
+ }
+
+ if ([children count] == 0) {
+ return [FEmptyNode emptyNode];
+ } else {
+ FImmutableSortedDictionary *childrenDict = [FImmutableSortedDictionary fromDictionary:children
+ withComparator:[FUtilities keyComparator]];
+ return [[FChildrenNode alloc] initWithPriority:priority children:childrenDict];
+ }
+ } else if([value isKindOfClass:[NSArray class]]) {
+ NSArray* aval = (NSArray *)value;
+ NSMutableDictionary* children = [NSMutableDictionary dictionaryWithCapacity:aval.count];
+
+ for(int i = 0; i < [aval count]; i++) {
+ NSString* key = [NSString stringWithFormat:@"%i", i];
+ [path addObject:key];
+ id<FNode> childNode = [FSnapshotUtilities nodeFrom:[aval objectAtIndex:i] priority:nil withValidationFrom:fn atDepth:depth + 1 path:path];
+ [path removeLastObject];
+
+ if (![childNode isEmpty]) {
+ children[key] = childNode;
+ }
+ }
+
+ if ([children count] == 0) {
+ return [FEmptyNode emptyNode];
+ } else {
+ FImmutableSortedDictionary *childrenDict = [FImmutableSortedDictionary fromDictionary:children
+ withComparator:[FUtilities keyComparator]];
+ return [[FChildrenNode alloc] initWithPriority:priority children:childrenDict];
+ }
+ } else {
+ NSRange range;
+ range.location = 0;
+ range.length = MIN(path.count, 50);
+ NSString* pathString = [[path subarrayWithRange:range] componentsJoinedByString:@"."];
+
+ @throw [[NSException alloc] initWithName:@"InvalidFirebaseData"
+ reason:[NSString stringWithFormat:@"(%@) Cannot store object of type %@ at %@. "
+ "Can only store objects of type NSNumber, NSString, NSDictionary, and NSArray.",
+ fn, [[value class] description], pathString] userInfo:nil];
+ }
+}
+
++ (FCompoundWrite *) compoundWriteFromDictionary:(NSDictionary *)values withValidationFrom:(NSString *)fn {
+ FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite];
+
+ NSMutableArray *updatePaths = [NSMutableArray arrayWithCapacity:values.count];
+ for (NSString *keyId in values) {
+ id value = values[keyId];
+ [FValidation validateFrom:fn validUpdateDictionaryKey:keyId withValue:value];
+
+ FPath* path = [FPath pathWithString:keyId];
+ id<FNode> node = [FSnapshotUtilities nodeFrom:value withValidationFrom:fn];
+
+ [updatePaths addObject:path];
+ compoundWrite = [compoundWrite addWrite:node atPath:path];
+ }
+
+ // Check that the update paths are not descendants of each other.
+ [updatePaths sortUsingComparator:^NSComparisonResult(FPath* left, FPath* right) {
+ return [left compare:right];
+ }];
+ FPath *prevPath = nil;
+ for (FPath *path in updatePaths) {
+ if (prevPath != nil && [prevPath contains:path]) {
+ @throw [[NSException alloc] initWithName:@"InvalidFirebaseData" reason:[NSString stringWithFormat:@"(%@) Invalid path in object. Path (%@) is an ancestor of (%@).", fn, prevPath, path] userInfo:nil];
+ }
+ prevPath = path;
+ }
+
+ return compoundWrite;
+}
+
++ (void)validatePriorityNode:(id <FNode>)priorityNode {
+ assert(priorityNode != nil);
+ if (priorityNode.isLeafNode) {
+ id val = priorityNode.val;
+ if ([val isKindOfClass:[NSDictionary class]]) {
+ NSDictionary* valDict __unused = (NSDictionary*)val;
+ NSAssert(valDict[kServerValueSubKey] != nil, @"Priority can't be object unless it's a deferred value");
+ } else {
+ NSString *jsType __unused = [FUtilities getJavascriptType:val];
+ NSAssert(jsType == kJavaScriptString || jsType == kJavaScriptNumber, @"Priority of unexpected type.");
+ }
+ } else {
+ NSAssert(priorityNode == [FMaxNode maxNode] || priorityNode.isEmpty, @"Priority of unexpected type.");
+ }
+ // Don't call getPriority() on MAX_NODE to avoid hitting assertion.
+ NSAssert(priorityNode == [FMaxNode maxNode] || priorityNode.getPriority.isEmpty,
+ @"Priority nodes can't have a priority of their own.");
+}
+
++ (void)appendHashRepresentationForLeafNode:(FLeafNode *)leafNode
+ toString:(NSMutableString *)string
+ hashVersion:(FDataHashVersion)hashVersion {
+ NSAssert(hashVersion == FDataHashVersionV1 || hashVersion == FDataHashVersionV2,
+ @"Unknown hash version: %lu", (unsigned long)hashVersion);
+ if (!leafNode.getPriority.isEmpty) {
+ [string appendString:@"priority:"];
+ [FSnapshotUtilities appendHashRepresentationForLeafNode:leafNode.getPriority toString:string hashVersion:hashVersion];
+ [string appendString:@":"];
+ }
+
+ NSString *jsType = [FUtilities getJavascriptType:leafNode.val];
+ [string appendString:jsType];
+ [string appendString:@":"];
+
+
+ if (jsType == kJavaScriptBoolean) {
+ NSString *boolString = [leafNode.val boolValue] ? kJavaScriptTrue : kJavaScriptFalse;
+ [string appendString:boolString];
+ } else if (jsType == kJavaScriptNumber) {
+ NSString *numberString = [FUtilities ieee754StringForNumber:leafNode.val];
+ [string appendString:numberString];
+ } else if (jsType == kJavaScriptString) {
+ if (hashVersion == FDataHashVersionV1) {
+ [string appendString:leafNode.val];
+ } else {
+ NSAssert(hashVersion == FDataHashVersionV2, @"Invalid hash version found");
+ [FSnapshotUtilities appendHashV2RepresentationForString:leafNode.val toString:string];
+ }
+ } else {
+ [NSException raise:NSInvalidArgumentException format:@"Unknown value for hashing: %@", leafNode];
+ }
+}
+
++ (void)appendHashV2RepresentationForString:(NSString *)string
+ toString:(NSMutableString *)mutableString {
+ string = [string stringByReplacingOccurrencesOfString:@"\\" withString:@"\\\\"];
+ string = [string stringByReplacingOccurrencesOfString:@"\"" withString:@"\\\""];
+ [mutableString appendString:@"\""];
+ [mutableString appendString:string];
+ [mutableString appendString:@"\""];
+}
+
++ (NSUInteger)estimateLeafNodeSize:(FLeafNode *)leafNode {
+ NSString *jsType = [FUtilities getJavascriptType:leafNode.val];
+ // These values are somewhat arbitrary, but we don't need an exact value so prefer performance over exact value
+ NSUInteger valueSize;
+ if (jsType == kJavaScriptNumber) {
+ valueSize = 8; // estimate each float with 8 bytes
+ } else if (jsType == kJavaScriptBoolean) {
+ valueSize = 4; // true or false need roughly 4 bytes
+ } else if (jsType == kJavaScriptString) {
+ valueSize = 2 + [leafNode.val length]; // add 2 for quotes
+ } else {
+ [NSException raise:NSInvalidArgumentException format:@"Unknown leaf type: %@", leafNode];
+ return 0;
+ }
+
+ if (leafNode.getPriority.isEmpty) {
+ return valueSize;
+ } else {
+ // Account for extra overhead due to the extra JSON object and the ".value" and ".priority" keys, colons, comma
+ NSUInteger leafPriorityOverhead = 2 + 8 + 11 + 2 + 1;
+ return leafPriorityOverhead + valueSize + [FSnapshotUtilities estimateLeafNodeSize:leafNode.getPriority];
+ }
+}
+
++ (NSUInteger)estimateSerializedNodeSize:(id<FNode>)node {
+ if ([node isEmpty]) {
+ return 4; // null keyword
+ } else if ([node isLeafNode]) {
+ return [FSnapshotUtilities estimateLeafNodeSize:node];
+ } else {
+ NSAssert([node isKindOfClass:[FChildrenNode class]], @"Unexpected node type: %@", [node class]);
+ __block NSUInteger sum = 1; // opening brackets
+ [((FChildrenNode *)node) enumerateChildrenAndPriorityUsingBlock:^(NSString *key, id<FNode>child, BOOL *stop) {
+ sum += key.length;
+ sum += 4; // quotes around key and colon and (comma or closing bracket)
+ sum += [FSnapshotUtilities estimateSerializedNodeSize:child];
+ }];
+ return sum;
+ }
+}
+
+@end
diff --git a/Firebase/Database/Utilities/FAtomicNumber.h b/Firebase/Database/Utilities/FAtomicNumber.h
new file mode 100644
index 0000000..589dc25
--- /dev/null
+++ b/Firebase/Database/Utilities/FAtomicNumber.h
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+@interface FAtomicNumber : NSObject
+
+- (NSNumber *) getAndIncrement;
+
+@end
diff --git a/Firebase/Database/Utilities/FAtomicNumber.m b/Firebase/Database/Utilities/FAtomicNumber.m
new file mode 100644
index 0000000..be0e537
--- /dev/null
+++ b/Firebase/Database/Utilities/FAtomicNumber.m
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FAtomicNumber.h"
+
+@interface FAtomicNumber() {
+ unsigned long number;
+}
+
+@property (nonatomic, strong) NSLock* lock;
+
+@end
+
+@implementation FAtomicNumber
+
+@synthesize lock;
+
+- (id)init
+{
+ self = [super init];
+ if (self) {
+ number = 1;
+ self.lock = [[NSLock alloc] init];
+ }
+ return self;
+}
+
+- (NSNumber *) getAndIncrement {
+ NSNumber* result;
+
+ // See: http://developer.apple.com/library/ios/#DOCUMENTATION/Cocoa/Conceptual/Multithreading/ThreadSafety/ThreadSafety.html#//apple_ref/doc/uid/10000057i-CH8-SW14 to improve, etc.
+
+ [self.lock lock];
+ result = [NSNumber numberWithUnsignedLong:number];
+ number = number + 1;
+ [self.lock unlock];
+
+ return result;
+}
+
+@end
diff --git a/Firebase/Database/Utilities/FEventEmitter.h b/Firebase/Database/Utilities/FEventEmitter.h
new file mode 100644
index 0000000..069e10f
--- /dev/null
+++ b/Firebase/Database/Utilities/FEventEmitter.h
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRDatabaseQuery.h"
+#import "FIRDatabaseConfig.h"
+#import "FTypedefs_Private.h"
+
+@interface FEventEmitter : NSObject
+
+- (id) initWithAllowedEvents:(NSArray *)theAllowedEvents queue:(dispatch_queue_t)queue;
+
+- (id) getInitialEventForType:(NSString *)eventType;
+- (void) triggerEventType:(NSString *)eventType data:(id)data;
+
+- (FIRDatabaseHandle)observeEventType:(NSString *)eventType withBlock:(fbt_void_id)block;
+- (void) removeObserverForEventType:(NSString *)eventType withHandle:(FIRDatabaseHandle)handle;
+
+- (void) validateEventType:(NSString *)eventType;
+
+@end
diff --git a/Firebase/Database/Utilities/FEventEmitter.m b/Firebase/Database/Utilities/FEventEmitter.m
new file mode 100644
index 0000000..f7c569b
--- /dev/null
+++ b/Firebase/Database/Utilities/FEventEmitter.m
@@ -0,0 +1,145 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+#import "FEventEmitter.h"
+#import "FUtilities.h"
+#import "FRepoManager.h"
+#import "FIRDatabaseQuery_Private.h"
+
+@interface FEventListener : NSObject
+
+@property (nonatomic, copy) fbt_void_id userCallback;
+@property (nonatomic) FIRDatabaseHandle handle;
+
+@end
+
+@implementation FEventListener
+
+@synthesize userCallback;
+@synthesize handle;
+
+@end
+
+
+@interface FEventEmitter ()
+
+@property (nonatomic, strong) NSArray *allowedEvents;
+@property (nonatomic, strong) NSMutableDictionary *listeners;
+@property (nonatomic, strong) dispatch_queue_t queue;
+
+@end
+
+
+@implementation FEventEmitter
+
+@synthesize allowedEvents;
+@synthesize listeners;
+
+- (id) initWithAllowedEvents:(NSArray *)theAllowedEvents queue:(dispatch_queue_t)queue {
+ if (theAllowedEvents == nil || [theAllowedEvents count] == 0) {
+ @throw [NSException exceptionWithName:@"AllowedEventsValidation" reason:@"FEventEmitters must be initialized with at least one valid event." userInfo:nil];
+ }
+
+ self = [super init];
+
+ if (self) {
+ self.allowedEvents = [theAllowedEvents copy];
+ self.listeners = [[NSMutableDictionary alloc] init];
+ self.queue = queue;
+ }
+
+ return self;
+}
+
+- (id) getInitialEventForType:(NSString *)eventType {
+ @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"You must override getInitialEvent: when subclassing FEventEmitter" userInfo:nil];
+}
+
+- (void) triggerEventType:(NSString *)eventType data:(id)data {
+ [self validateEventType:eventType];
+ NSMutableDictionary *eventTypeListeners = [self.listeners objectForKey:eventType];
+ for (FEventListener *listener in eventTypeListeners) {
+ [self triggerListener:listener withData:data];
+ }
+}
+
+- (void) triggerListener:(FEventListener *)listener withData:(id)data {
+ // TODO, should probably get this from FRepo or something although it ends up being the same. (Except maybe for testing)
+ if (listener.userCallback) {
+ dispatch_async(self.queue, ^{
+ listener.userCallback(data);
+ });
+ }
+}
+
+- (FIRDatabaseHandle)observeEventType:(NSString *)eventType withBlock:(fbt_void_id)block {
+ [self validateEventType:eventType];
+
+ // Create listener
+ FEventListener *listener = [[FEventListener alloc] init];
+ listener.handle = [[FUtilities LUIDGenerator] integerValue];
+ listener.userCallback = block; // copies block automatically
+
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ [self addEventListener:listener forEventType:eventType];
+ });
+
+ return listener.handle;
+}
+
+- (void) addEventListener:(FEventListener *)listener forEventType:(NSString *)eventType {
+ // Get or initializer listeners map [FIRDatabaseHandle -> callback block] for eventType
+ NSMutableArray *eventTypeListeners = [self.listeners objectForKey:eventType];
+ if (eventTypeListeners == nil) {
+ eventTypeListeners = [[NSMutableArray alloc] init];
+ [self.listeners setObject:eventTypeListeners forKey:eventType];
+ }
+
+ // Add listener and fire the current event for this listener
+ [eventTypeListeners addObject:listener];
+ id initialData = [self getInitialEventForType:eventType];
+ [self triggerListener:listener withData:initialData];
+}
+
+- (void) removeObserverForEventType:(NSString *)eventType withHandle:(FIRDatabaseHandle)handle {
+ [self validateEventType:eventType];
+
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ [self removeEventListenerWithHandle:handle forEventType:eventType];
+ });
+}
+
+- (void)removeEventListenerWithHandle:(FIRDatabaseHandle)handle forEventType:(NSString *)eventType {
+ NSMutableArray *eventTypeListeners = [self.listeners objectForKey:eventType];
+ for (FEventListener *listener in [eventTypeListeners copy]) {
+ if (handle == NSNotFound || handle == listener.handle) {
+ [eventTypeListeners removeObject:listener];
+ }
+ }
+}
+
+
+- (void) validateEventType:(NSString *)eventType {
+ if ([self.allowedEvents indexOfObject:eventType] == NSNotFound) {
+ @throw [NSException exceptionWithName:@"InvalidEventType"
+ reason:[NSString stringWithFormat:@"%@ is not a valid event type. %@ is the list of valid events.",
+ eventType, self.allowedEvents]
+ userInfo:nil];
+ }
+}
+
+@end
diff --git a/Firebase/Database/Utilities/FNextPushId.h b/Firebase/Database/Utilities/FNextPushId.h
new file mode 100644
index 0000000..2da54f0
--- /dev/null
+++ b/Firebase/Database/Utilities/FNextPushId.h
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+@interface FNextPushId : NSObject
+
++ (NSString *) get:(NSTimeInterval)now;
+
+@end
diff --git a/Firebase/Database/Utilities/FNextPushId.m b/Firebase/Database/Utilities/FNextPushId.m
new file mode 100644
index 0000000..af54e3d
--- /dev/null
+++ b/Firebase/Database/Utilities/FNextPushId.m
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FNextPushId.h"
+#import "FUtilities.h"
+
+static NSString *const PUSH_CHARS = @"-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz";
+
+@implementation FNextPushId
+
++ (NSString *) get:(NSTimeInterval)currentTime {
+ static long long lastPushTime = 0;
+ static int lastRandChars[12];
+
+ long long now = (long long)(currentTime * 1000);
+
+ BOOL duplicateTime = now == lastPushTime;
+ lastPushTime = now;
+
+ unichar timeStampChars[8];
+ for(int i = 7; i >= 0; i--) {
+ timeStampChars[i] = [PUSH_CHARS characterAtIndex:(now % 64)];
+ now = (long long)floor(now / 64);
+ }
+
+ NSMutableString* id = [[NSMutableString alloc] init];
+ [id appendString:[NSString stringWithCharacters:timeStampChars length:8]];
+
+
+ if(!duplicateTime) {
+ for(int i = 0; i < 12; i++) {
+ lastRandChars[i] = (int)floor(arc4random() % 64);
+ }
+ }
+ else {
+ int i = 0;
+ for(i = 11; i >= 0 && lastRandChars[i] == 63; i--) {
+ lastRandChars[i] = 0;
+ }
+ lastRandChars[i]++;
+ }
+
+ for(int i = 0; i < 12; i++) {
+ [id appendFormat:@"%C", [PUSH_CHARS characterAtIndex:lastRandChars[i]]];
+ }
+
+ return [NSString stringWithString:id];
+}
+
+@end
diff --git a/Firebase/Database/Utilities/FParsedUrl.h b/Firebase/Database/Utilities/FParsedUrl.h
new file mode 100644
index 0000000..7145f86
--- /dev/null
+++ b/Firebase/Database/Utilities/FParsedUrl.h
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FRepoInfo.h"
+#import "FPath.h"
+
+@interface FParsedUrl : NSObject
+
+@property (nonatomic, strong) FRepoInfo* repoInfo;
+@property (nonatomic, strong) FPath* path;
+
+@end
diff --git a/Firebase/Database/Utilities/FParsedUrl.m b/Firebase/Database/Utilities/FParsedUrl.m
new file mode 100644
index 0000000..eb83330
--- /dev/null
+++ b/Firebase/Database/Utilities/FParsedUrl.m
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FParsedUrl.h"
+
+@implementation FParsedUrl
+
+@synthesize repoInfo;
+@synthesize path;
+
+@end
diff --git a/Firebase/Database/Utilities/FStringUtilities.h b/Firebase/Database/Utilities/FStringUtilities.h
new file mode 100644
index 0000000..34ac9af
--- /dev/null
+++ b/Firebase/Database/Utilities/FStringUtilities.h
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+@interface FStringUtilities : NSObject
+
++ (NSString *) base64EncodedSha1:(NSString *)str;
++ (NSString *) urlDecoded:(NSString *)url;
++ (NSString *) urlEncoded:(NSString *)url;
++ (NSString *) sanitizedForUserAgent:(NSString *)str;
+
+@end
diff --git a/Firebase/Database/Utilities/FStringUtilities.m b/Firebase/Database/Utilities/FStringUtilities.m
new file mode 100644
index 0000000..dff58e0
--- /dev/null
+++ b/Firebase/Database/Utilities/FStringUtilities.m
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <CommonCrypto/CommonDigest.h>
+#import "FStringUtilities.h"
+#import "NSData+SRB64Additions.h"
+
+@implementation FStringUtilities
+
+// http://stackoverflow.com/questions/3468268/objective-c-sha1
+// http://stackoverflow.com/questions/7310457/ios-objective-c-sha-1-and-base64-problem
++ (NSString *) base64EncodedSha1:(NSString *)str {
+ const char *cstr = [str cStringUsingEncoding:NSUTF8StringEncoding];
+ // NSString reports length in characters, but we want it in bytes, which strlen will give us.
+ unsigned long dataLen = strlen(cstr);
+ NSData *data = [NSData dataWithBytes:cstr length:dataLen];
+ uint8_t digest[CC_SHA1_DIGEST_LENGTH];
+ CC_SHA1(data.bytes, (unsigned int)data.length, digest);
+ NSData* output = [[NSData alloc] initWithBytes:digest length:CC_SHA1_DIGEST_LENGTH];
+ return [FSRUtilities base64EncodedStringFromData:output];
+}
+
++ (NSString *) urlDecoded:(NSString *)url {
+ NSString* replaced = [url stringByReplacingOccurrencesOfString:@"+" withString:@" "];
+ NSString* decoded = [replaced stringByRemovingPercentEncoding];
+ // This is kind of a hack, but is generally how the js client works. We could run into trouble if
+ // some piece is a correctly escaped %-sequence, and another isn't. But, that's bad input anyways...
+ if (decoded) {
+ return decoded;
+ } else {
+ return replaced;
+ }
+}
+
++ (NSString *) urlEncoded:(NSString *)url {
+ // Didn't seem like there was an Apple NSCharacterSet that had our version of the encoding
+ // So I made my own, following RFC 2396 https://www.ietf.org/rfc/rfc2396.txt
+ // allowedCharacters = alphanum | "-" | "_" | "~"
+ NSCharacterSet *allowedCharacters = [NSCharacterSet characterSetWithCharactersInString:@"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_~"];
+ return [url stringByAddingPercentEncodingWithAllowedCharacters:allowedCharacters];
+}
+
++ (NSString *) sanitizedForUserAgent:(NSString *)str {
+ return [str stringByReplacingOccurrencesOfString:@"/|_" withString:@"|" options:NSRegularExpressionSearch range:NSMakeRange(0, [str length])];
+}
+
+
+@end
diff --git a/Firebase/Database/Utilities/FTypedefs.h b/Firebase/Database/Utilities/FTypedefs.h
new file mode 100644
index 0000000..4a24ca5
--- /dev/null
+++ b/Firebase/Database/Utilities/FTypedefs.h
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+#ifndef Firebase_FTypedefs_h
+#define Firebase_FTypedefs_h
+
+/**
+ * Stub...
+ */
+@class FIRDataSnapshot;
+@class FIRDatabaseReference;
+@class FAuthData;
+@protocol FNode;
+
+// fbt = Firebase Block Typedef
+
+typedef void (^fbt_void_void)(void);
+typedef void (^fbt_void_datasnapshot_nsstring) (FIRDataSnapshot *snapshot, NSString *prevName);
+typedef void (^fbt_void_datasnapshot) (FIRDataSnapshot *snapshot);
+typedef void (^fbt_void_user)(FAuthData *user);
+typedef void (^fbt_void_nsstring_id)(NSString* status, id data);
+typedef void (^fbt_void_nserror_id)(NSError* error, id data);
+typedef void (^fbt_void_nserror)(NSError *error);
+typedef void (^fbt_void_nserror_ref)(NSError* error, FIRDatabaseReference * ref);
+typedef void (^fbt_void_nserror_user)(NSError* error, FAuthData * user);
+typedef void (^fbt_void_nserror_json)(NSError* error, NSDictionary* json);
+typedef void (^fbt_void_nsdictionary)(NSDictionary *data);
+typedef id (^fbt_id_node_nsstring)(id<FNode> node, NSString* childName);
+
+#endif
diff --git a/Firebase/Database/Utilities/FUtilities.h b/Firebase/Database/Utilities/FUtilities.h
new file mode 100644
index 0000000..f5e312f
--- /dev/null
+++ b/Firebase/Database/Utilities/FUtilities.h
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+#import "FIRLogger.h"
+#import "FParsedUrl.h"
+
+@interface FUtilities : NSObject
+
++ (NSArray *) splitString:(NSString *)str intoMaxSize:(const unsigned int)size;
++ (NSNumber *) LUIDGenerator;
++ (FParsedUrl *) parseUrl:(NSString *)url;
++ (NSString *) getJavascriptType:(id)obj;
++ (NSError *) errorForStatus:(NSString *)status andReason:(NSString *)reason;
++ (NSNumber *) intForString:(NSString *)string;
++ (NSString *) ieee754StringForNumber:(NSNumber *)val;
++ (void) setLoggingEnabled:(BOOL)enabled;
++ (BOOL) getLoggingEnabled;
+
++ (NSString*) minName;
++ (NSString*) maxName;
++ (NSComparisonResult) compareKey:(NSString *)a toKey:(NSString *)b;
++ (NSComparator) stringComparator;
++ (NSComparator) keyComparator;
+
++ (double)randomDouble;
+
+@end
+
+typedef enum {
+ FLogLevelDebug = 1,
+ FLogLevelInfo = 2,
+ FLogLevelWarn = 3,
+ FLogLevelError = 4,
+ FLogLevelNone = 5
+} FLogLevel;
+
+// Log tags
+FOUNDATION_EXPORT NSString *const kFPersistenceLogTag;
+
+#define FFLog(code, format, ...) FFDebug((code), (format), ##__VA_ARGS__)
+
+#define FFDebug(code, format, ...) do { \
+ if (FFIsLoggingEnabled(FLogLevelDebug)) { \
+ FIRLogDebug(kFIRLoggerDatabase, (code), (format), ##__VA_ARGS__); \
+ } \
+} while(0)
+
+#define FFInfo(code, format, ...) do { \
+ if (FFIsLoggingEnabled(FLogLevelInfo)) { \
+ FIRLogError(kFIRLoggerDatabase, (code), (format), ##__VA_ARGS__); \
+ } \
+} while(0)
+
+#define FFWarn(code, format, ...) do { \
+ if (FFIsLoggingEnabled(FLogLevelWarn)) { \
+ FIRLogWarning(kFIRLoggerDatabase, (code), (format), ##__VA_ARGS__); \
+ } \
+} while(0)
+
+BOOL FFIsLoggingEnabled(FLogLevel logLevel);
+void firebaseUncaughtExceptionHandler(NSException *exception);
+void firebaseJobsTroll(void);
diff --git a/Firebase/Database/Utilities/FUtilities.m b/Firebase/Database/Utilities/FUtilities.m
new file mode 100644
index 0000000..7c25e3b
--- /dev/null
+++ b/Firebase/Database/Utilities/FUtilities.m
@@ -0,0 +1,389 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FUtilities.h"
+#import "FStringUtilities.h"
+#import "FConstants.h"
+#import "FAtomicNumber.h"
+
+#define ARC4RANDOM_MAX 0x100000000
+#define INTEGER_32_MIN (-2147483648)
+#define INTEGER_32_MAX 2147483647
+
+#pragma mark -
+#pragma mark C functions
+
+static FLogLevel logLevel = FLogLevelInfo; // Default log level is info
+static NSMutableDictionary* options = nil;
+
+BOOL FFIsLoggingEnabled(FLogLevel level) {
+ return level >= logLevel;
+}
+
+void firebaseJobsTroll(void) {
+ FFLog(@"I-RDB095001", @"password super secret; JFK conspiracy; Hello there! Having fun digging through Firebase? We're always hiring! jobs@firebase.com");
+}
+
+#pragma mark -
+#pragma mark Private property and singleton specification
+
+@interface FUtilities() {
+
+}
+
+@property (nonatomic, strong) FAtomicNumber* localUid;
+
++ (FUtilities*)singleton;
+
+@end
+
+@implementation FUtilities
+
+@synthesize localUid;
+
+- (id)init
+{
+ self = [super init];
+ if (self) {
+ self.localUid = [[FAtomicNumber alloc] init];
+ }
+ return self;
+}
+
+// TODO: We really want to be able to set the log level
++ (void) setLoggingEnabled:(BOOL)enabled {
+ logLevel = enabled ? FLogLevelDebug : FLogLevelInfo;
+}
+
++ (BOOL) getLoggingEnabled {
+ return logLevel == FLogLevelDebug;
+}
+
++ (FUtilities*) singleton
+{
+ static dispatch_once_t pred = 0;
+ __strong static id _sharedObject = nil;
+ dispatch_once(&pred, ^{
+ _sharedObject = [[self alloc] init]; // or some other init method
+ });
+ return _sharedObject;
+}
+
+// Refactor as a category of NSString
++ (NSArray *) splitString:(NSString *) str intoMaxSize:(const unsigned int) size {
+ if(str.length <= size) {
+ return [NSArray arrayWithObject:str];
+ }
+
+ NSMutableArray* dataSegs = [[NSMutableArray alloc] init];
+ for(int c = 0; c < str.length; c += size) {
+ if (c + size > str.length) {
+ int rangeStart = c;
+ unsigned long rangeLength = size - ((c + size) - str.length);
+ [dataSegs addObject:[str substringWithRange:NSMakeRange(rangeStart, rangeLength)]];
+ }
+ else {
+ int rangeStart = c;
+ int rangeLength = size;
+ [dataSegs addObject:[str substringWithRange:NSMakeRange(rangeStart, rangeLength)]];
+ }
+ }
+ return dataSegs;
+}
+
++ (NSNumber *) LUIDGenerator {
+ FUtilities* f = [FUtilities singleton];
+ return [f.localUid getAndIncrement];
+}
+
++ (NSString *) decodePath:(NSString *)pathString {
+ NSMutableArray* decodedPieces = [[NSMutableArray alloc] init];
+ NSArray* pieces = [pathString componentsSeparatedByString:@"/"];
+ for (NSString* piece in pieces) {
+ if (piece.length > 0) {
+ [decodedPieces addObject:[FStringUtilities urlDecoded:piece]];
+ }
+ }
+ return [NSString stringWithFormat:@"/%@", [decodedPieces componentsJoinedByString:@"/"]];
+}
+
++ (FParsedUrl *) parseUrl:(NSString *)url {
+ NSString* original = url;
+ //NSURL* n = [[NSURL alloc] initWithString:url]
+
+ NSString* host;
+ NSString* namespace;
+ bool secure;
+
+ NSString* scheme = nil;
+ FPath* path = nil;
+ NSRange colonIndex = [url rangeOfString:@"//"];
+ if (colonIndex.location != NSNotFound) {
+ scheme = [url substringToIndex:colonIndex.location - 1];
+ url = [url substringFromIndex:colonIndex.location + 2];
+ }
+ NSInteger slashIndex = [url rangeOfString:@"/"].location;
+ if (slashIndex == NSNotFound) {
+ slashIndex = url.length;
+ }
+
+ host = [[url substringToIndex:slashIndex] lowercaseString];
+ if (slashIndex >= url.length) {
+ url = @"";
+ } else {
+ url = [url substringFromIndex:slashIndex + 1];
+ }
+
+ NSArray *parts = [host componentsSeparatedByString:@"."];
+ if([parts count] == 3) {
+ NSInteger colonIndex = [[parts objectAtIndex:2] rangeOfString:@":"].location;
+ if (colonIndex != NSNotFound) {
+ // we have a port, use the provided scheme
+ secure = [scheme isEqualToString:@"https"];
+ } else {
+ secure = YES;
+ }
+
+ namespace = [[parts objectAtIndex:0] lowercaseString];
+ NSString* pathString = [self decodePath:[NSString stringWithFormat:@"/%@", url]];
+ path = [[FPath alloc] initWith:pathString];
+ }
+ else {
+ [NSException raise:@"No Firebase database specified." format:@"No Firebase database found for input: %@", url];
+ }
+
+ FRepoInfo* repoInfo = [[FRepoInfo alloc] initWithHost:host isSecure:secure withNamespace:namespace];
+
+ FFLog(@"I-RDB095002", @"---> Parsed (%@) to: (%@,%@); ns=(%@); path=(%@)", original, [repoInfo description], [repoInfo connectionURL], repoInfo.namespace, [path description]);
+
+ FParsedUrl* parsedUrl = [[FParsedUrl alloc] init];
+ parsedUrl.repoInfo = repoInfo;
+ parsedUrl.path = path;
+
+ return parsedUrl;
+}
+
+/*
+ case str: JString => priString + "string:" + str.s;
+ case bool: JBool => priString + "boolean:" + bool.value;
+ case double: JDouble => priString + "number:" + double.num;
+ case int: JInt => priString + "number:" + int.num;
+ case _ => {
+ error("Leaf node has value '" + data.value + "' of invalid type '" + data.value.getClass.toString + "'");
+ "";
+ }
+ */
+
++ (NSString *) getJavascriptType:(id)obj {
+ if ([obj isKindOfClass:[NSDictionary class]]) {
+ return kJavaScriptObject;
+ } else if([obj isKindOfClass:[NSString class]]) {
+ return kJavaScriptString;
+ }
+ else if ([obj isKindOfClass:[NSNumber class]]) {
+ // We used to just compare to @encode(BOOL) as suggested at
+ // http://stackoverflow.com/questions/2518761/get-type-of-nsnumber, but on arm64, @encode(BOOL) returns "B"
+ // instead of "c" even though objCType still returns 'c' (signed char). So check both.
+ if(strcmp([obj objCType], @encode(BOOL)) == 0 ||
+ strcmp([obj objCType], @encode(signed char)) == 0) {
+ return kJavaScriptBoolean;
+ }
+ else {
+ return kJavaScriptNumber;
+ }
+ }
+ else {
+ return kJavaScriptNull;
+ }
+}
+
++ (NSError *) errorForStatus:(NSString *)status andReason:(NSString *)reason {
+ static dispatch_once_t pred = 0;
+ __strong static NSDictionary* errorMap = nil;
+ __strong static NSDictionary* errorCodes = nil;
+ dispatch_once(&pred, ^{
+ errorMap = @{
+ @"permission_denied": @"Permission Denied",
+ @"unavailable": @"Service is unavailable",
+ kFErrorWriteCanceled: @"Write cancelled by user"
+ };
+ errorCodes = @{
+ @"permission_denied": @1,
+ @"unavailable": @2,
+ kFErrorWriteCanceled: @3
+ };
+ });
+
+ if ([status isEqualToString:kFWPResponseForActionStatusOk]) {
+ return nil;
+ } else {
+ NSInteger code;
+ NSString* desc = nil;
+ if (reason) {
+ desc = reason;
+ } else if ([errorMap objectForKey:status] != nil) {
+ desc = [errorMap objectForKey:status];
+ } else {
+ desc = status;
+ }
+
+ if ([errorCodes objectForKey:status] != nil) {
+ NSNumber* num = [errorCodes objectForKey:status];
+ code = [num integerValue];
+ } else {
+ // XXX what to do here?
+ code = 9999;
+ }
+
+ return [[NSError alloc] initWithDomain:kFErrorDomain code:code userInfo:@{NSLocalizedDescriptionKey: desc}];
+ }
+}
+
++ (NSNumber *) intForString:(NSString *)string {
+ static NSCharacterSet *notDigits = nil;
+ if (!notDigits) {
+ notDigits = [[NSCharacterSet decimalDigitCharacterSet] invertedSet];
+ }
+ if ([string rangeOfCharacterFromSet:notDigits].length == 0) {
+ NSInteger num;
+ NSScanner* scanner = [NSScanner scannerWithString:string];
+ if ([scanner scanInteger:&num]) {
+ return [NSNumber numberWithInteger:num];
+ }
+ }
+ return nil;
+}
+
++ (NSString *) ieee754StringForNumber:(NSNumber *)val {
+ double d = [val doubleValue];
+ NSData* data = [NSData dataWithBytes:&d length:sizeof(double)];
+ NSMutableString* str = [[NSMutableString alloc] init];
+ const unsigned char* buffer = (const unsigned char*)[data bytes];
+ for (int i = 0; i < data.length; i++) {
+ unsigned char byte = buffer[7 - i];
+ [str appendFormat:@"%02x", byte];
+ }
+ return str;
+}
+
+static inline BOOL tryParseStringToInt(__unsafe_unretained NSString* str, NSInteger* integer) {
+ // First do some cheap checks (NOTE: The below checks are significantly faster than an equivalent regex :-( ).
+ NSUInteger length = str.length;
+ if (length > 11 || length == 0) {
+ return NO;
+ }
+ long long value = 0;
+ BOOL negative = NO;
+ NSUInteger i = 0;
+ if ([str characterAtIndex:0] == '-') {
+ if (length == 1) {
+ return NO;
+ }
+ negative = YES;
+ i = 1;
+ }
+ for(; i < length; i++) {
+ unichar c = [str characterAtIndex:i];
+ // Must be a digit, or '-' if it's the first char.
+ if (c < '0' || c > '9') {
+ return NO;
+ } else {
+ int charValue = c - '0';
+ value = value*10 + charValue;
+ }
+ }
+
+ value = (negative) ? -value : value;
+
+ if (value < INTEGER_32_MIN || value > INTEGER_32_MAX) {
+ return NO;
+ } else {
+ *integer = (NSInteger)value;
+ return YES;
+ }
+}
+
++ (NSString *) maxName {
+ static dispatch_once_t once;
+ static NSString *maxName;
+ dispatch_once(&once, ^{
+ maxName = [[NSString alloc] initWithFormat:@"[MAX_NAME]"];
+ });
+ return maxName;
+}
+
++ (NSString *) minName {
+ static dispatch_once_t once;
+ static NSString *minName;
+ dispatch_once(&once, ^{
+ minName = [[NSString alloc] initWithFormat:@"[MIN_NAME]"];
+ });
+ return minName;
+}
+
++ (NSComparisonResult) compareKey:(NSString *)a toKey:(NSString *)b {
+ if (a == b) {
+ return NSOrderedSame;
+ } else if (a == [FUtilities minName] || b == [FUtilities maxName]) {
+ return NSOrderedAscending;
+ } else if (b == [FUtilities minName] || a == [FUtilities maxName]) {
+ return NSOrderedDescending;
+ } else {
+ NSInteger aAsInt, bAsInt;
+ if (tryParseStringToInt(a, &aAsInt)) {
+ if (tryParseStringToInt(b, &bAsInt)) {
+ if (aAsInt > bAsInt) {
+ return NSOrderedDescending;
+ } else if (aAsInt < bAsInt) {
+ return NSOrderedAscending;
+ } else if (a.length > b.length) {
+ return NSOrderedDescending;
+ } else if (a.length < b.length) {
+ return NSOrderedAscending;
+ } else {
+ return NSOrderedSame;
+ }
+ } else {
+ return (NSComparisonResult) NSOrderedAscending;
+ }
+ } else if (tryParseStringToInt(b, &bAsInt)) {
+ return (NSComparisonResult) NSOrderedDescending;
+ } else {
+ // Perform literal character by character search to prevent a > b && b > a issues.
+ // Note that calling -(NSString *)decomposedStringWithCanonicalMapping also works.
+ return [a compare:b options:NSLiteralSearch];
+ }
+ }
+}
+
++ (NSComparator) keyComparator {
+ return ^NSComparisonResult(__unsafe_unretained NSString *a, __unsafe_unretained NSString *b) {
+ return [FUtilities compareKey:a toKey:b];
+ };
+}
+
++ (NSComparator) stringComparator {
+ return ^NSComparisonResult(__unsafe_unretained NSString *a, __unsafe_unretained NSString *b) {
+ return [a compare:b];
+ };
+}
+
++ (double) randomDouble {
+ return ((double) arc4random() / ARC4RANDOM_MAX);
+}
+
+@end
+
diff --git a/Firebase/Database/Utilities/FValidation.h b/Firebase/Database/Utilities/FValidation.h
new file mode 100644
index 0000000..faa8f76
--- /dev/null
+++ b/Firebase/Database/Utilities/FValidation.h
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+#import "FPath.h"
+#import "FIRDataEventType.h"
+#import "FParsedUrl.h"
+#import "FTypedefs.h"
+
+@interface FValidation : NSObject
+
++ (void) validateFrom:(NSString *)fn writablePath:(FPath *)path;
++ (void) validateFrom:(NSString *)fn knownEventType:(FIRDataEventType)event;
++ (void) validateFrom:(NSString *)fn validPathString:(NSString *)pathString;
++ (void) validateFrom:(NSString *)fn validRootPathString:(NSString *)pathString;
++ (void) validateFrom:(NSString *)fn validKey:(NSString *)key;
++ (void) validateFrom:(NSString *)fn validURL:(FParsedUrl *)parsedUrl;
+
++ (void) validateToken:(NSString *)token;
+
+// Functions for handling passing errors back
++ (void) handleError:(NSError *)error withUserCallback:(fbt_void_nserror_id)userCallback;
++ (void) handleError:(NSError *)error withSuccessCallback:(fbt_void_nserror)userCallback;
+
+// Functions used for validating while creating snapshots in FSnapshotUtilities
++ (BOOL) validateFrom:(NSString*)fn isValidLeafValue:(id)value withPath:(NSArray*)path;
++ (void) validateFrom:(NSString*)fn validDictionaryKey:(id)keyId withPath:(NSArray*)path;
++ (void) validateFrom:(NSString*)fn validUpdateDictionaryKey:(id)keyId withValue:(id)value;
++ (void) validateFrom:(NSString*)fn isValidPriorityValue:(id)value withPath:(NSArray*)path;
++ (BOOL) validatePriorityValue:value;
+
+@end
diff --git a/Firebase/Database/Utilities/FValidation.m b/Firebase/Database/Utilities/FValidation.m
new file mode 100644
index 0000000..c4c6b2b
--- /dev/null
+++ b/Firebase/Database/Utilities/FValidation.m
@@ -0,0 +1,312 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FValidation.h"
+#import "FConstants.h"
+#import "FParsedUrl.h"
+#import "FTypedefs.h"
+
+
+// Have to escape: * ? + [ ( ) { } ^ $ | \ . /
+// See: https://developer.apple.com/library/mac/#documentation/Foundation/Reference/NSRegularExpression_Class/Reference/Reference.html
+
+NSString *const kInvalidPathCharacters = @"[].#$";
+NSString *const kInvalidKeyCharacters = @"[].#$/";
+
+@implementation FValidation
+
++ (void) validateFrom:(NSString *)fn writablePath:(FPath *)path {
+ if([[path getFront] isEqualToString:kDotInfoPrefix]) {
+ @throw [[NSException alloc] initWithName:@"WritablePathValidation" reason:[NSString stringWithFormat:@"(%@) failed to path %@: Can't modify data under %@", fn, [path description], kDotInfoPrefix] userInfo:nil];
+ }
+}
+
++ (void) validateFrom:(NSString*)fn knownEventType:(FIRDataEventType)event {
+ switch (event) {
+ case FIRDataEventTypeValue:
+ case FIRDataEventTypeChildAdded:
+ case FIRDataEventTypeChildChanged:
+ case FIRDataEventTypeChildMoved:
+ case FIRDataEventTypeChildRemoved:
+ return;
+ break;
+ default:
+ @throw [[NSException alloc] initWithName:@"KnownEventTypeValidation" reason:[NSString stringWithFormat:@"(%@) Unknown event type: %d", fn, (int) event] userInfo:nil];
+ break;
+ }
+}
+
++ (BOOL) isValidPathString:(NSString *)pathString {
+ static dispatch_once_t token;
+ static NSCharacterSet *badPathChars = nil;
+ dispatch_once(&token, ^{
+ badPathChars = [NSCharacterSet characterSetWithCharactersInString:kInvalidPathCharacters];
+ });
+ return pathString != nil && [pathString length] != 0 &&
+ [pathString rangeOfCharacterFromSet:badPathChars].location == NSNotFound;
+}
+
++ (void) validateFrom:(NSString *)fn validPathString:(NSString *)pathString {
+ if(! [self isValidPathString:pathString]) {
+ @throw [[NSException alloc] initWithName:@"InvalidPathValidation" reason:[NSString stringWithFormat:@"(%@) Must be a non-empty string and not contain '.' '#' '$' '[' or ']'", fn] userInfo:nil];
+ }
+}
+
++ (void) validateFrom:(NSString *)fn validRootPathString:(NSString *)pathString {
+ static dispatch_once_t token;
+ static NSRegularExpression *dotInfoRegex = nil;
+ dispatch_once(&token, ^{
+ dotInfoRegex = [NSRegularExpression regularExpressionWithPattern:@"^\\/*\\.info(\\/|$)" options:0 error:nil];
+ });
+
+ NSString *tempPath = pathString;
+ // HACK: Obj-C regex are kinda' slow. Do a plain string search first before bothering with the regex.
+ if ([pathString rangeOfString:@".info"].location != NSNotFound) {
+ tempPath = [dotInfoRegex stringByReplacingMatchesInString:pathString options:0 range:NSMakeRange(0, pathString.length) withTemplate:@"/"];
+ }
+ [self validateFrom:fn validPathString:tempPath];
+}
+
++ (BOOL) isValidKey:(NSString *)key {
+ static dispatch_once_t token;
+ static NSCharacterSet *badKeyChars = nil;
+ dispatch_once(&token, ^{
+ badKeyChars = [NSCharacterSet characterSetWithCharactersInString:kInvalidKeyCharacters];
+ });
+ return key != nil && key.length > 0 && [key rangeOfCharacterFromSet:badKeyChars].location == NSNotFound;
+}
+
++ (void) validateFrom:(NSString *)fn validKey:(NSString *)key {
+ if (![self isValidKey:key]) {
+ @throw [[NSException alloc] initWithName:@"InvalidKeyValidation" reason:[NSString stringWithFormat:@"(%@) Must be a non-empty string and not contain '/' '.' '#' '$' '[' or ']'", fn] userInfo:nil];
+ }
+}
+
++ (void) validateFrom:(NSString *)fn validURL:(FParsedUrl *)parsedUrl {
+ NSString* pathString = [parsedUrl.path description];
+ [self validateFrom:fn validRootPathString:pathString];
+}
+
+#pragma mark -
+#pragma mark Authentication validation
+
++ (BOOL) stringNonempty:(NSString *)str {
+ return str != nil && ![str isKindOfClass:[NSNull class]] && str.length > 0;
+}
+
++ (void) validateToken:(NSString *)token {
+ if (![FValidation stringNonempty:token]) {
+ [NSException raise:NSInvalidArgumentException format:@"Can't have empty string or nil for custom token"];
+ }
+}
+
+#pragma mark -
+#pragma mark Handling authentication errors
+
+/**
+* This function immediately calls the callback.
+* It assumes that it is not on FirebaseWorker thread.
+* It assumes it's on a user-controlled thread.
+*/
++ (void) handleError:(NSError *)error withUserCallback:(fbt_void_nserror_id)userCallback {
+ if (userCallback) {
+ userCallback(error, nil);
+ }
+}
+
+/**
+* This function immediately calls the callback.
+* It assumes that it is not on FirebaseWorker thread.
+* It assumes it's on a user-controlled thread.
+*/
++ (void) handleError:(NSError *)error withSuccessCallback:(fbt_void_nserror)userCallback {
+ if (userCallback) {
+ userCallback(error);
+ }
+}
+
+#pragma mark -
+#pragma mark Snapshot validation
+
++ (BOOL) validateFrom:(NSString*)fn isValidLeafValue:(id)value withPath:(NSArray*)path {
+ if ([value isKindOfClass:[NSString class]]) {
+ // Try to avoid conversion to bytes if possible
+ NSString* theString = value;
+ if ([theString maximumLengthOfBytesUsingEncoding:NSUTF8StringEncoding] > kFirebaseMaxLeafSize &&
+ [theString lengthOfBytesUsingEncoding:NSUTF8StringEncoding] > kFirebaseMaxLeafSize) {
+ NSRange range;
+ range.location = 0;
+ range.length = MIN(path.count, 50);
+ NSString* pathString = [[path subarrayWithRange:range] componentsJoinedByString:@"."];
+ @throw [[NSException alloc] initWithName:@"InvalidFirebaseData" reason:[NSString stringWithFormat:@"(%@) String exceeds max size of %u utf8 bytes: %@", fn, (int)kFirebaseMaxLeafSize, pathString] userInfo:nil];
+ }
+ return YES;
+ }
+
+ else if ([value isKindOfClass:[NSNumber class]]) {
+ // Cannot store NaN, but otherwise can store NSNumbers.
+ if ([[NSDecimalNumber notANumber] isEqualToNumber:value]) {
+ NSRange range;
+ range.location = 0;
+ range.length = MIN(path.count, 50);
+ NSString* pathString = [[path subarrayWithRange:range] componentsJoinedByString:@"."];
+ @throw [[NSException alloc] initWithName:@"InvalidFirebaseData" reason:[NSString stringWithFormat:@"(%@) Cannot store NaN at path: %@.", fn, pathString] userInfo:nil];
+ }
+ return YES;
+ }
+
+ else if ([value isKindOfClass:[NSDictionary class]]) {
+ NSDictionary* dval = value;
+ if (dval[kServerValueSubKey] != nil) {
+ if ([dval count] > 1) {
+ NSRange range;
+ range.location = 0;
+ range.length = MIN(path.count, 50);
+ NSString* pathString = [[path subarrayWithRange:range] componentsJoinedByString:@"."];
+ @throw [[NSException alloc] initWithName:@"InvalidFirebaseData" reason:[NSString stringWithFormat:@"(%@) Cannot store other keys with server value keys.%@.", fn, pathString] userInfo:nil];
+ }
+ return YES;
+ }
+ return NO;
+ }
+
+ else if (value == [NSNull null] || value == nil) {
+ // Null is valid type to store at leaf
+ return YES;
+ }
+
+ return NO;
+}
+
++ (NSString*) parseAndValidateKey:(id)keyId fromFunction:(NSString*)fn path:(NSArray*)path {
+ if (![keyId isKindOfClass:[NSString class]]) {
+ NSRange range;
+ range.location = 0;
+ range.length = MIN(path.count, 50);
+ NSString* pathString = [[path subarrayWithRange:range] componentsJoinedByString:@"."];
+ @throw [[NSException alloc] initWithName:@"InvalidFirebaseData" reason:[NSString stringWithFormat:@"(%@) Non-string keys are not allowed in object at path: %@", fn, pathString] userInfo:nil];
+ }
+ return (NSString*)keyId;
+}
+
++ (void) validateFrom:(NSString*)fn validDictionaryKey:(id)keyId withPath:(NSArray*)path {
+ NSString *key = [self parseAndValidateKey:keyId fromFunction:fn path:path];
+ if (![key isEqualToString:kPayloadPriority] && ![key isEqualToString:kPayloadValue] && ![key isEqualToString:kServerValueSubKey] && ![FValidation isValidKey:key]) {
+ NSRange range;
+ range.location = 0;
+ range.length = MIN(path.count, 50);
+ NSString *pathString = [[path subarrayWithRange:range] componentsJoinedByString:@"."];
+ @throw [[NSException alloc] initWithName:@"InvalidFirebaseData" reason:[NSString stringWithFormat:@"(%@) Invalid key in object at path: %@. Keys must be non-empty and cannot contain '/' '.' '#' '$' '[' or ']'", fn, pathString] userInfo:nil];
+ }
+}
+
++ (void) validateFrom:(NSString*)fn validUpdateDictionaryKey:(id)keyId withValue:(id)value {
+ FPath *path = [FPath pathWithString:[self parseAndValidateKey:keyId fromFunction:fn path:@[]]];
+ __block NSInteger keyNum = 0;
+ [path enumerateComponentsUsingBlock:^void (NSString *key, BOOL *stop) {
+ if ([key isEqualToString:kPayloadPriority] && keyNum == [path length] - 1) {
+ [self validateFrom:fn isValidPriorityValue:value withPath:@[]];
+ } else {
+ keyNum++;
+
+ if (![FValidation isValidKey:key]) {
+ @throw [[NSException alloc] initWithName:@"InvalidFirebaseData" reason:[NSString stringWithFormat:@"(%@) Invalid key in object. Keys must be non-empty and cannot contain '.' '#' '$' '[' or ']'", fn] userInfo:nil];
+ }
+ }
+ }];
+}
+
++ (void) validateFrom:(NSString*)fn isValidPriorityValue:(id)value withPath:(NSArray*)path {
+ [self validateFrom:fn isValidPriorityValue:value withPath:path throwError:YES];
+}
+
+/**
+* Returns YES if priority is valid.
+*/
++ (BOOL)validatePriorityValue:value {
+ return [self validateFrom:nil isValidPriorityValue:value withPath:nil throwError:NO];
+}
+
+/**
+* Helper for validating priorities. If passed YES for throwError, it'll throw descriptive errors on validation
+* problems. Else, it'll just return YES/NO.
+*/
++ (BOOL) validateFrom:(NSString*)fn isValidPriorityValue:(id)value withPath:(NSArray*)path throwError:(BOOL)throwError {
+ if ([value isKindOfClass:[NSNumber class]]) {
+ if ([[NSDecimalNumber notANumber] isEqualToNumber:value]) {
+ if (throwError) {
+ NSRange range;
+ range.location = 0;
+ range.length = MIN(path.count, 50);
+ NSString *pathString = [[path subarrayWithRange:range] componentsJoinedByString:@"."];
+ @throw [[NSException alloc] initWithName:@"InvalidFirebaseData" reason:[NSString stringWithFormat:@"(%@) Cannot store NaN as priority at path: %@.", fn, pathString] userInfo:nil];
+ } else {
+ return NO;
+ }
+ } else if (value == (id) kCFBooleanFalse || value == (id) kCFBooleanTrue) {
+ if (throwError) {
+ NSRange range;
+ range.location = 0;
+ range.length = MIN(path.count, 50);
+ NSString *pathString = [[path subarrayWithRange:range] componentsJoinedByString:@"."];
+ @throw [[NSException alloc] initWithName:@"InvalidFirebaseData" reason:[NSString stringWithFormat:@"(%@) Cannot store true/false as priority at path: %@.", fn, pathString] userInfo:nil];
+ } else {
+ return NO;
+ }
+ }
+ }
+ else if ([value isKindOfClass:[NSDictionary class]]) {
+ NSDictionary *dval = value;
+ if (dval[kServerValueSubKey] != nil) {
+ if ([dval count] > 1) {
+ if (throwError) {
+ NSRange range;
+ range.location = 0;
+ range.length = MIN(path.count, 50);
+ NSString *pathString = [[path subarrayWithRange:range] componentsJoinedByString:@"."];
+ @throw [[NSException alloc] initWithName:@"InvalidFirebaseData" reason:[NSString stringWithFormat:@"(%@) Cannot store other keys with server value keys as priority at path: %@.", fn, pathString] userInfo:nil];
+ } else {
+ return NO;
+ }
+ }
+ } else {
+ if (throwError) {
+ NSRange range;
+ range.location = 0;
+ range.length = MIN(path.count, 50);
+ NSString *pathString = [[path subarrayWithRange:range] componentsJoinedByString:@"."];
+ @throw [[NSException alloc] initWithName:@"InvalidFirebaseData" reason:[NSString stringWithFormat:@"(%@) Cannot store an NSDictionary as priority at path: %@.", fn, pathString] userInfo:nil];
+ } else {
+ return NO;
+ }
+ }
+ }
+ else if ([value isKindOfClass:[NSArray class]]) {
+ if (throwError) {
+ NSRange range;
+ range.location = 0;
+ range.length = MIN(path.count, 50);
+ NSString *pathString = [[path subarrayWithRange:range] componentsJoinedByString:@"."];
+ @throw [[NSException alloc] initWithName:@"InvalidFirebaseData" reason:[NSString stringWithFormat:@"(%@) Cannot store an NSArray as priority at path: %@.", fn, pathString] userInfo:nil];
+ } else {
+ return NO;
+ }
+ }
+
+ // It's valid!
+ return YES;
+}
+@end
diff --git a/Firebase/Database/Utilities/NSString+FURLUtils.h b/Firebase/Database/Utilities/NSString+FURLUtils.h
new file mode 100644
index 0000000..7bd39bc
--- /dev/null
+++ b/Firebase/Database/Utilities/NSString+FURLUtils.h
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+@interface NSString (FURLUtils)
+
+- (NSString *) urlEncoded;
+- (NSString *) urlDecoded;
+
+@end
diff --git a/Firebase/Database/Utilities/NSString+FURLUtils.m b/Firebase/Database/Utilities/NSString+FURLUtils.m
new file mode 100644
index 0000000..2e018c8
--- /dev/null
+++ b/Firebase/Database/Utilities/NSString+FURLUtils.m
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "NSString+FURLUtils.h"
+
+@implementation NSString (FURLUtils)
+
+- (NSString *) urlDecoded {
+ NSString* replaced = [self stringByReplacingOccurrencesOfString:@"+" withString:@" "];
+ NSString* decoded = [replaced stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
+ // This is kind of a hack, but is generally how the js client works. We could run into trouble if
+ // some piece is a correctly escaped %-sequence, and another isn't. But, that's bad input anyways...
+ if (decoded) {
+ return decoded;
+ } else {
+ return replaced;
+ }
+}
+
+- (NSString *) urlEncoded {
+ CFStringRef urlString = CFURLCreateStringByAddingPercentEscapes(NULL, (__bridge CFStringRef)self, NULL, (CFStringRef)@"!*'\"();:@&=+$,/?%#[]% ", kCFStringEncodingUTF8);
+ return (__bridge NSString *) urlString;
+}
+
+@end
diff --git a/Firebase/Database/Utilities/Tuples/FTupleBoolBlock.h b/Firebase/Database/Utilities/Tuples/FTupleBoolBlock.h
new file mode 100644
index 0000000..bceeed2
--- /dev/null
+++ b/Firebase/Database/Utilities/Tuples/FTupleBoolBlock.h
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+#import "FTypedefs.h"
+
+@interface FTupleBoolBlock : NSObject
+
+@property (nonatomic, readwrite) BOOL boolean;
+@property (nonatomic, copy) fbt_void_void block;
+
+@end
diff --git a/Firebase/Database/Utilities/Tuples/FTupleBoolBlock.m b/Firebase/Database/Utilities/Tuples/FTupleBoolBlock.m
new file mode 100644
index 0000000..c4cd8bf
--- /dev/null
+++ b/Firebase/Database/Utilities/Tuples/FTupleBoolBlock.m
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FTupleBoolBlock.h"
+
+@implementation FTupleBoolBlock
+
+@synthesize boolean;
+@synthesize block;
+
+@end
diff --git a/Firebase/Database/Utilities/Tuples/FTupleCallbackStatus.h b/Firebase/Database/Utilities/Tuples/FTupleCallbackStatus.h
new file mode 100644
index 0000000..6ec2375
--- /dev/null
+++ b/Firebase/Database/Utilities/Tuples/FTupleCallbackStatus.h
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+#import "FTypedefs_Private.h"
+
+@interface FTupleCallbackStatus : NSObject
+@property (nonatomic, copy) fbt_void_nsstring_nsstring block;
+@property (nonatomic) NSString* status;
+@property (nonatomic) NSString* errorReason;
+@end
diff --git a/Firebase/Database/Utilities/Tuples/FTupleCallbackStatus.m b/Firebase/Database/Utilities/Tuples/FTupleCallbackStatus.m
new file mode 100644
index 0000000..05914bf
--- /dev/null
+++ b/Firebase/Database/Utilities/Tuples/FTupleCallbackStatus.m
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FTupleCallbackStatus.h"
+
+@implementation FTupleCallbackStatus
+@synthesize block;
+@synthesize status;
+@end
diff --git a/Firebase/Database/Utilities/Tuples/FTupleFirebase.h b/Firebase/Database/Utilities/Tuples/FTupleFirebase.h
new file mode 100644
index 0000000..ff84bbb
--- /dev/null
+++ b/Firebase/Database/Utilities/Tuples/FTupleFirebase.h
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+#import "FIRDatabaseReference.h"
+
+@interface FTupleFirebase : NSObject
+
+@property (nonatomic, strong) FIRDatabaseReference * one;
+@property (nonatomic, strong) FIRDatabaseReference * two;
+@property (nonatomic, strong) FIRDatabaseReference * three;
+
+@end
diff --git a/Firebase/Database/Utilities/Tuples/FTupleFirebase.m b/Firebase/Database/Utilities/Tuples/FTupleFirebase.m
new file mode 100644
index 0000000..3956f8b
--- /dev/null
+++ b/Firebase/Database/Utilities/Tuples/FTupleFirebase.m
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FTupleFirebase.h"
+
+@implementation FTupleFirebase
+
+@synthesize one;
+@synthesize two;
+@synthesize three;
+
+@end
diff --git a/Firebase/Database/Utilities/Tuples/FTupleNodePath.h b/Firebase/Database/Utilities/Tuples/FTupleNodePath.h
new file mode 100644
index 0000000..fbf62c7
--- /dev/null
+++ b/Firebase/Database/Utilities/Tuples/FTupleNodePath.h
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+#import "FPath.h"
+#import "FNode.h"
+
+@interface FTupleNodePath : NSObject
+
+@property (nonatomic, strong) FPath* path;
+@property (nonatomic, strong) id<FNode> node;
+
+- (id) initWithNode:(id<FNode>)aNode andPath:(FPath *)aPath;
+
+@end
diff --git a/Firebase/Database/Utilities/Tuples/FTupleNodePath.m b/Firebase/Database/Utilities/Tuples/FTupleNodePath.m
new file mode 100644
index 0000000..eefc0c2
--- /dev/null
+++ b/Firebase/Database/Utilities/Tuples/FTupleNodePath.m
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FTupleNodePath.h"
+
+@implementation FTupleNodePath
+
+@synthesize path;
+@synthesize node;
+
+- (id) initWithNode:(id<FNode>)aNode andPath:(FPath *)aPath {
+ self = [super init];
+ if (self) {
+ self.path = aPath;
+ self.node = aNode;
+ }
+ return self;
+}
+
+@end
diff --git a/Firebase/Database/Utilities/Tuples/FTupleObjectNode.h b/Firebase/Database/Utilities/Tuples/FTupleObjectNode.h
new file mode 100644
index 0000000..6fcb746
--- /dev/null
+++ b/Firebase/Database/Utilities/Tuples/FTupleObjectNode.h
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+#import "FNode.h"
+
+@interface FTupleObjectNode : NSObject
+
+- (id)initWithObject:(id)aObj andNode:(id<FNode>)aNode;
+
+@property (nonatomic, strong) id<FNode> node;
+@property (nonatomic, strong) id obj;
+
+@end
diff --git a/Firebase/Database/Utilities/Tuples/FTupleObjectNode.m b/Firebase/Database/Utilities/Tuples/FTupleObjectNode.m
new file mode 100644
index 0000000..4c533b0
--- /dev/null
+++ b/Firebase/Database/Utilities/Tuples/FTupleObjectNode.m
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#import "FTupleObjectNode.h"
+
+@implementation FTupleObjectNode
+
+@synthesize obj;
+@synthesize node;
+
+- (id)initWithObject:(id)aObj andNode:(id<FNode>)aNode {
+ self = [super init];
+ if (self) {
+ self.obj = aObj;
+ self.node = aNode;
+ }
+ return self;
+}
+
+@end
diff --git a/Firebase/Database/Utilities/Tuples/FTupleObjects.h b/Firebase/Database/Utilities/Tuples/FTupleObjects.h
new file mode 100644
index 0000000..4ff1fcf
--- /dev/null
+++ b/Firebase/Database/Utilities/Tuples/FTupleObjects.h
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+@interface FTupleObjects : NSObject
+
+@property (nonatomic, strong) id objA;
+@property (nonatomic, strong) id objB;
+
+@end
diff --git a/Firebase/Database/Utilities/Tuples/FTupleObjects.m b/Firebase/Database/Utilities/Tuples/FTupleObjects.m
new file mode 100644
index 0000000..a9e4c88
--- /dev/null
+++ b/Firebase/Database/Utilities/Tuples/FTupleObjects.m
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FTupleObjects.h"
+
+@implementation FTupleObjects
+
+@synthesize objA;
+@synthesize objB;
+
+@end
diff --git a/Firebase/Database/Utilities/Tuples/FTupleOnDisconnect.h b/Firebase/Database/Utilities/Tuples/FTupleOnDisconnect.h
new file mode 100644
index 0000000..91ad5e4
--- /dev/null
+++ b/Firebase/Database/Utilities/Tuples/FTupleOnDisconnect.h
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+#import "FTypedefs_Private.h"
+
+@interface FTupleOnDisconnect : NSObject
+
+@property (strong, nonatomic) NSString* pathString;
+@property (strong, nonatomic) NSString* action;
+@property (strong, nonatomic) id data;
+@property (strong, nonatomic) fbt_void_nsstring_nsstring onComplete;
+
+@end
diff --git a/Firebase/Database/Utilities/Tuples/FTupleOnDisconnect.m b/Firebase/Database/Utilities/Tuples/FTupleOnDisconnect.m
new file mode 100644
index 0000000..bd45822
--- /dev/null
+++ b/Firebase/Database/Utilities/Tuples/FTupleOnDisconnect.m
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FTupleOnDisconnect.h"
+
+@implementation FTupleOnDisconnect
+
+@synthesize pathString;
+@synthesize action;
+@synthesize data;
+@synthesize onComplete;
+
+@end
diff --git a/Firebase/Database/Utilities/Tuples/FTuplePathValue.h b/Firebase/Database/Utilities/Tuples/FTuplePathValue.h
new file mode 100644
index 0000000..f7ed423
--- /dev/null
+++ b/Firebase/Database/Utilities/Tuples/FTuplePathValue.h
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+@class FPath;
+
+@interface FTuplePathValue : NSObject
+@property (nonatomic, strong, readonly) FPath *path;
+@property (nonatomic, strong, readonly) id value;
+- (id) initWithPath:(FPath *)aPath value:(id)aValue;
+@end
diff --git a/Firebase/Database/Utilities/Tuples/FTuplePathValue.m b/Firebase/Database/Utilities/Tuples/FTuplePathValue.m
new file mode 100644
index 0000000..49240aa
--- /dev/null
+++ b/Firebase/Database/Utilities/Tuples/FTuplePathValue.m
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FTuplePathValue.h"
+#import "FPath.h"
+
+@interface FTuplePathValue ()
+@property (nonatomic, strong, readwrite) id value;
+@property (nonatomic, strong, readwrite) FPath *path;
+@end
+
+@implementation FTuplePathValue
+@synthesize path;
+@synthesize value;
+
+- (id) initWithPath:(FPath *)aPath value:(id)aValue {
+ self = [super init];
+ if (self) {
+ self.value = aValue;
+ self.path = aPath;
+ }
+ return self;
+}
+
+@end
diff --git a/Firebase/Database/Utilities/Tuples/FTupleRemovedQueriesEvents.h b/Firebase/Database/Utilities/Tuples/FTupleRemovedQueriesEvents.h
new file mode 100644
index 0000000..f986916
--- /dev/null
+++ b/Firebase/Database/Utilities/Tuples/FTupleRemovedQueriesEvents.h
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+@interface FTupleRemovedQueriesEvents : NSObject
+/**
+* `FIRDatabaseQuery`s removed with [SyncPoint removeEventRegistration:]
+*/
+@property (nonatomic, strong, readonly) NSArray *removedQueries;
+/**
+* cancel events as FEvent
+*/
+@property (nonatomic, strong, readonly) NSArray *cancelEvents;
+
+- (id) initWithRemovedQueries:(NSArray *)removed cancelEvents:(NSArray *)events;
+@end
diff --git a/Firebase/Database/Utilities/Tuples/FTupleRemovedQueriesEvents.m b/Firebase/Database/Utilities/Tuples/FTupleRemovedQueriesEvents.m
new file mode 100644
index 0000000..818d16b
--- /dev/null
+++ b/Firebase/Database/Utilities/Tuples/FTupleRemovedQueriesEvents.m
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FTupleRemovedQueriesEvents.h"
+
+@interface FTupleRemovedQueriesEvents ()
+@property (nonatomic, strong, readwrite) NSArray *removedQueries;
+@property (nonatomic, strong, readwrite) NSArray *cancelEvents;
+@end
+
+@implementation FTupleRemovedQueriesEvents
+@synthesize removedQueries;
+@synthesize cancelEvents;
+
+- (id) initWithRemovedQueries:(NSArray *)removed cancelEvents:(NSArray *)events {
+ self = [super init];
+ if (self) {
+ self.removedQueries = removed;
+ self.cancelEvents = events;
+ }
+ return self;
+}
+
+@end
diff --git a/Firebase/Database/Utilities/Tuples/FTupleSetIdPath.h b/Firebase/Database/Utilities/Tuples/FTupleSetIdPath.h
new file mode 100644
index 0000000..5133d6d
--- /dev/null
+++ b/Firebase/Database/Utilities/Tuples/FTupleSetIdPath.h
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+#import "FPath.h"
+
+@interface FTupleSetIdPath : NSObject
+
+- (id) initWithSetId:(NSNumber *)aSetId andPath:(FPath *)aPath;
+
+@property (strong, nonatomic) NSNumber* setId;
+@property (strong, nonatomic) FPath* path;
+
+@end
diff --git a/Firebase/Database/Utilities/Tuples/FTupleSetIdPath.m b/Firebase/Database/Utilities/Tuples/FTupleSetIdPath.m
new file mode 100644
index 0000000..5d3312b
--- /dev/null
+++ b/Firebase/Database/Utilities/Tuples/FTupleSetIdPath.m
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FTupleSetIdPath.h"
+
+@implementation FTupleSetIdPath
+
+@synthesize path;
+@synthesize setId;
+
+- (id) initWithSetId:(NSNumber *)aSetId andPath:(FPath *)aPath {
+ self = [super init];
+ if (self) {
+ self.setId = aSetId;
+ self.path = aPath;
+ }
+ return self;
+}
+
+@end
diff --git a/Firebase/Database/Utilities/Tuples/FTupleStringNode.h b/Firebase/Database/Utilities/Tuples/FTupleStringNode.h
new file mode 100644
index 0000000..e3fec80
--- /dev/null
+++ b/Firebase/Database/Utilities/Tuples/FTupleStringNode.h
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+#import "FNode.h"
+
+@interface FTupleStringNode : NSObject
+
+- (id)initWithString:(NSString *)aString andNode:(id<FNode>)aNode;
+
+@property (nonatomic, strong) id<FNode> node;
+@property (nonatomic, strong) NSString* string;
+
+@end
diff --git a/Firebase/Database/Utilities/Tuples/FTupleStringNode.m b/Firebase/Database/Utilities/Tuples/FTupleStringNode.m
new file mode 100644
index 0000000..f058a8e
--- /dev/null
+++ b/Firebase/Database/Utilities/Tuples/FTupleStringNode.m
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FTupleStringNode.h"
+
+@implementation FTupleStringNode
+
+@synthesize string;
+@synthesize node;
+
+- (id)initWithString:(NSString *)aString andNode:(id<FNode>)aNode {
+ self = [super init];
+ if (self) {
+ self.string = aString;
+ self.node = aNode;
+ }
+ return self;
+}
+
+
+@end
diff --git a/Firebase/Database/Utilities/Tuples/FTupleTSN.h b/Firebase/Database/Utilities/Tuples/FTupleTSN.h
new file mode 100644
index 0000000..bc62b2d
--- /dev/null
+++ b/Firebase/Database/Utilities/Tuples/FTupleTSN.h
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+#import "FTupleStringNode.h"
+
+@interface FTupleTSN : NSObject
+
+@property (nonatomic, strong) FTupleStringNode* from;
+@property (nonatomic, strong) FTupleStringNode* to;
+
+@end
diff --git a/Firebase/Database/Utilities/Tuples/FTupleTSN.m b/Firebase/Database/Utilities/Tuples/FTupleTSN.m
new file mode 100644
index 0000000..348c319
--- /dev/null
+++ b/Firebase/Database/Utilities/Tuples/FTupleTSN.m
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FTupleTSN.h"
+
+@implementation FTupleTSN
+
+@synthesize from;
+@synthesize to;
+
+@end
diff --git a/Firebase/Database/Utilities/Tuples/FTupleTransaction.h b/Firebase/Database/Utilities/Tuples/FTupleTransaction.h
new file mode 100644
index 0000000..c9dcf4b
--- /dev/null
+++ b/Firebase/Database/Utilities/Tuples/FTupleTransaction.h
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+#import "FPath.h"
+#import "FTypedefs_Private.h"
+#import "FTypedefs.h"
+
+@interface FTupleTransaction : NSObject
+
+@property (nonatomic, strong) FPath* path;
+@property (nonatomic, copy) fbt_transactionresult_mutabledata update;
+@property (nonatomic, copy) fbt_void_nserror_bool_datasnapshot onComplete;
+@property (nonatomic) FTransactionStatus status;
+
+/**
+* Used when combining transaction at different locations to figure out which one goes first.
+*/
+@property (nonatomic, strong) NSNumber* order;
+/**
+* Whether to raise local events for this transaction
+*/
+@property (nonatomic) BOOL applyLocally;
+
+/**
+* Count how many times we've retried the transaction
+*/
+@property (nonatomic) int retryCount;
+
+/**
+* Function to call to clean up our listener
+*/
+@property (nonatomic, copy) fbt_void_void unwatcher;
+
+/**
+* Stores why a transaction was aborted
+*/
+@property (nonatomic, strong, readonly) NSString* abortStatus;
+@property (nonatomic, strong, readonly) NSString* abortReason;
+
+- (void)setAbortStatus:(NSString *)abortStatus reason:(NSString *)reason;
+- (NSError *)abortError;
+
+@property (nonatomic, strong) NSNumber *currentWriteId;
+
+/**
+* Stores the input snapshot, before the update
+*/
+@property (nonatomic, strong) id<FNode> currentInputSnapshot;
+
+/**
+* Stores the unresolved (for server values) output snapshot, after the update
+*/
+@property (nonatomic, strong) id<FNode> currentOutputSnapshotRaw;
+
+/**
+ * Stores the resolved (for server values) output snapshot, after the update
+ */
+@property (nonatomic, strong) id<FNode> currentOutputSnapshotResolved;
+
+@end
diff --git a/Firebase/Database/Utilities/Tuples/FTupleTransaction.m b/Firebase/Database/Utilities/Tuples/FTupleTransaction.m
new file mode 100644
index 0000000..bcff54e
--- /dev/null
+++ b/Firebase/Database/Utilities/Tuples/FTupleTransaction.m
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FTupleTransaction.h"
+#import "FUtilities.h"
+
+@interface FTupleTransaction ()
+
+@property (nonatomic, strong) NSString *abortStatus;
+@property (nonatomic, strong) NSString *abortReason;
+
+@end
+
+@implementation FTupleTransaction
+
+- (void)setAbortStatus:(NSString *)abortStatus reason:(NSString *)reason {
+ self.abortStatus = abortStatus;
+ self.abortReason = reason;
+}
+
+- (NSError *)abortError {
+ return (self.abortStatus != nil) ? [FUtilities errorForStatus:self.abortStatus andReason:self.abortReason] : nil;
+}
+
+@end
diff --git a/Firebase/Database/Utilities/Tuples/FTupleUserCallback.h b/Firebase/Database/Utilities/Tuples/FTupleUserCallback.h
new file mode 100644
index 0000000..d598217
--- /dev/null
+++ b/Firebase/Database/Utilities/Tuples/FTupleUserCallback.h
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+#import "FTypedefs.h"
+#import "FQueryParams.h"
+
+@interface FTupleUserCallback : NSObject
+
+- (id) initWithHandle:(NSUInteger)handle;
+
+@property (nonatomic, copy) fbt_void_datasnapshot_nsstring datasnapshotPrevnameCallback;
+@property (nonatomic, copy) fbt_void_datasnapshot datasnapshotCallback;
+@property (nonatomic, copy) fbt_void_nserror cancelCallback;
+@property (nonatomic, copy) FQueryParams* queryParams;
+@property (nonatomic) NSUInteger handle;
+
+@end
diff --git a/Firebase/Database/Utilities/Tuples/FTupleUserCallback.m b/Firebase/Database/Utilities/Tuples/FTupleUserCallback.m
new file mode 100644
index 0000000..dc33bbd
--- /dev/null
+++ b/Firebase/Database/Utilities/Tuples/FTupleUserCallback.m
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FTupleUserCallback.h"
+
+@implementation FTupleUserCallback
+
+@synthesize datasnapshotCallback;
+@synthesize datasnapshotPrevnameCallback;
+@synthesize cancelCallback;
+@synthesize queryParams;
+@synthesize handle;
+
+- (id) initWithHandle:(NSUInteger)theHandle {
+ self = [super init];
+ if (self) {
+ self.handle = theHandle;
+ }
+ return self;
+}
+
+@end
diff --git a/Firebase/Database/module.modulemap b/Firebase/Database/module.modulemap
new file mode 100644
index 0000000..28b323e
--- /dev/null
+++ b/Firebase/Database/module.modulemap
@@ -0,0 +1,13 @@
+framework module FirebaseDatabase {
+ umbrella header "FirebaseDatabase.h"
+
+ export *
+ module * { export * }
+
+ link framework "CFNetwork"
+ link framework "Security"
+ link framework "SystemConfiguration"
+
+ link "c++"
+ link "icucore"
+}
diff --git a/Firebase/Database/third_party/SocketRocket/FSRWebSocket.h b/Firebase/Database/third_party/SocketRocket/FSRWebSocket.h
new file mode 100644
index 0000000..dfda376
--- /dev/null
+++ b/Firebase/Database/third_party/SocketRocket/FSRWebSocket.h
@@ -0,0 +1,107 @@
+//
+// Copyright 2012 Square Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+#import <Foundation/Foundation.h>
+#import <Security/SecCertificate.h>
+
+typedef enum {
+ SR_CONNECTING = 0,
+ SR_OPEN = 1,
+ SR_CLOSING = 2,
+ SR_CLOSED = 3,
+
+} FSRReadyState;
+
+@class FSRWebSocket;
+
+extern NSString *const FSRWebSocketErrorDomain;
+
+@protocol FSRWebSocketDelegate;
+
+@interface FSRWebSocket : NSObject <NSStreamDelegate>
+
+@property (nonatomic, weak) id <FSRWebSocketDelegate> delegate;
+
+@property (nonatomic, readonly) FSRReadyState readyState;
+@property (nonatomic, readonly, retain) NSURL *url;
+
+// This returns the negotiated protocol.
+// It will be niluntil after the handshake completes.
+@property (nonatomic, readonly, copy) NSString *protocol;
+
+// Protocols should be an array of strings that turn into Sec-WebSocket-Protocol
+- (id)initWithURLRequest:(NSURLRequest *)request protocols:(NSArray *)protocols queue:(dispatch_queue_t)queue andUserAgent:(NSString *)userAgent;
+- (id)initWithURLRequest:(NSURLRequest *)request protocols:(NSArray *)protocols;
+- (id)initWithURLRequest:(NSURLRequest *)request queue:(dispatch_queue_t)queue andUserAgent:(NSString *)userAgent;
+- (id)initWithURLRequest:(NSURLRequest *)request;
+
+// Some helper constructors
+- (id)initWithURL:(NSURL *)url protocols:(NSArray *)protocols;
+- (id)initWithURL:(NSURL *)url;
+
+// Delegate queue will be dispatch_main_queue by default.
+// You cannot set both OperationQueue and dispatch_queue.
+- (void)setDelegateOperationQueue:(NSOperationQueue*) queue;
+- (void)setDelegateDispatchQueue:(dispatch_queue_t)queue;
+
+// By default, it will schedule itself on +[NSRunLoop SR_networkRunLoop] using defaultModes.
+- (void)scheduleInRunLoop:(NSRunLoop *)aRunLoop forMode:(NSString *)mode;
+- (void)unscheduleFromRunLoop:(NSRunLoop *)aRunLoop forMode:(NSString *)mode;
+
+// SRWebSockets are intended one-time-use only. Open should be called once and only once
+- (void)open;
+
+- (void)close;
+- (void)closeWithCode:(NSInteger)code reason:(NSString *)reason;
+
+// Send a UTF8 String or Data
+- (void)send:(id)data;
+
+@end
+
+@protocol FSRWebSocketDelegate <NSObject>
+
+// message will either be an NSString if the server is using text
+// or NSData if the server is using binary
+- (void)webSocket:(FSRWebSocket *)webSocket didReceiveMessage:(id)message;
+
+@optional
+
+- (void)webSocketDidOpen:(FSRWebSocket *)webSocket;
+- (void)webSocket:(FSRWebSocket *)webSocket didFailWithError:(NSError *)error;
+- (void)webSocket:(FSRWebSocket *)webSocket didCloseWithCode:(NSInteger)code reason:(NSString *)reason wasClean:(BOOL)wasClean;
+
+@end
+
+
+@interface NSURLRequest (FCertificateAdditions)
+
+@property (nonatomic, retain, readonly) NSArray *FSR_SSLPinnedCertificates;
+
+@end
+
+
+@interface NSMutableURLRequest (FCertificateAdditions)
+
+@property (nonatomic, retain) NSArray *FSR_SSLPinnedCertificates;
+
+@end
+
+@interface NSRunLoop (FSRWebSocket)
+
++ (NSRunLoop *)FSR_networkRunLoop;
+
+@end
diff --git a/Firebase/Database/third_party/SocketRocket/FSRWebSocket.m b/Firebase/Database/third_party/SocketRocket/FSRWebSocket.m
new file mode 100644
index 0000000..c2b395c
--- /dev/null
+++ b/Firebase/Database/third_party/SocketRocket/FSRWebSocket.m
@@ -0,0 +1,1848 @@
+//
+// Copyright 2012 Square Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+#import "FSRWebSocket.h"
+
+#if TARGET_OS_IPHONE
+#define HAS_ICU
+#endif
+
+#import <sys/socket.h>
+
+#ifdef HAS_ICU
+#import <unicode/utf8.h>
+#endif
+
+#if TARGET_OS_IPHONE
+#import <Endian.h>
+#else
+#import <CoreServices/CoreServices.h>
+#endif
+
+#import <CommonCrypto/CommonDigest.h>
+#import <Security/SecRandom.h>
+#import "fbase64.h"
+#import "NSData+SRB64Additions.h"
+
+#if OS_OBJECT_USE_OBJC_RETAIN_RELEASE
+#define sr_dispatch_retain(x)
+#define sr_dispatch_release(x)
+#define maybe_bridge(x) ((__bridge void *) x)
+#else
+#define sr_dispatch_retain(x) dispatch_retain(x)
+#define sr_dispatch_release(x) dispatch_release(x)
+#define maybe_bridge(x) (x)
+#endif
+
+typedef enum {
+ SROpCodeTextFrame = 0x1,
+ SROpCodeBinaryFrame = 0x2,
+ //3-7Reserved
+ SROpCodeConnectionClose = 0x8,
+ SROpCodePing = 0x9,
+ SROpCodePong = 0xA,
+ //B-F reserved
+} FSROpCode;
+
+typedef enum {
+ SRStatusCodeNormal = 1000,
+ SRStatusCodeGoingAway = 1001,
+ SRStatusCodeProtocolError = 1002,
+ SRStatusCodeUnhandledType = 1003,
+ // 1004 reserved
+ SRStatusNoStatusReceived = 1005,
+ // 1004-1006 reserved
+ SRStatusCodeInvalidUTF8 = 1007,
+ SRStatusCodePolicyViolated = 1008,
+ SRStatusCodeMessageTooBig = 1009,
+} FSRStatusCode;
+
+typedef struct {
+ BOOL fin;
+// BOOL rsv1;
+// BOOL rsv2;
+// BOOL rsv3;
+ uint8_t opcode;
+ BOOL masked;
+ uint64_t payload_length;
+} frame_header;
+
+static NSString *const SRWebSocketAppendToSecKeyString = @"258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
+
+static inline int32_t validate_dispatch_data_partial_string(NSData *data);
+static inline void SRFastLog(NSString *format, ...);
+
+@interface NSData (FSRWebSocket)
+
+- (NSString *)stringBySHA1ThenBase64Encoding;
+
+@end
+
+
+@interface NSString (FSRWebSocket)
+
+- (NSString *)stringBySHA1ThenBase64Encoding;
+
+@end
+
+
+@interface NSURL (FSRWebSocket)
+
+// The origin isn't really applicable for a native application
+// So instead, just map ws -> http and wss -> https
+- (NSString *)SR_origin;
+
+@end
+
+@interface _FSRRunLoopThread : NSThread
+
+@property (nonatomic, readonly) NSRunLoop *runLoop;
+
+@end
+
+static NSString *newSHA1String(const char *bytes, size_t length) {
+ uint8_t md[CC_SHA1_DIGEST_LENGTH];
+
+ CC_SHA1(bytes, (int)length, md);
+
+ size_t buffer_size = ((sizeof(md) * 3 + 2) / 2);
+
+ char *buffer = (char *)malloc(buffer_size);
+
+ int len = f_b64_ntop(md, CC_SHA1_DIGEST_LENGTH, buffer, buffer_size);
+ if (len == -1) {
+ free(buffer);
+ return nil;
+ } else{
+ return [[NSString alloc] initWithBytesNoCopy:buffer length:len encoding:NSASCIIStringEncoding freeWhenDone:YES];
+ }
+}
+
+@implementation NSData (FSRWebSocket)
+
+- (NSString *)stringBySHA1ThenBase64Encoding;
+{
+ return newSHA1String(self.bytes, self.length);
+}
+
+@end
+
+
+@implementation NSString (FSRWebSocket)
+
+- (NSString *)stringBySHA1ThenBase64Encoding;
+{
+ return newSHA1String(self.UTF8String, self.length);
+}
+
+@end
+
+NSString *const FSRWebSocketErrorDomain = @"FSRWebSocketErrorDomain";
+
+// Returns number of bytes consumed. returning 0 means you didn't match.
+// Sends bytes to callback handler;
+typedef size_t (^stream_scanner)(NSData *collected_data);
+
+typedef void (^data_callback)(FSRWebSocket *webSocket, NSData *data);
+
+@interface FSRIOConsumer : NSObject {
+ stream_scanner _scanner;
+ data_callback _handler;
+ size_t _bytesNeeded;
+ BOOL _readToCurrentFrame;
+ BOOL _unmaskBytes;
+}
+@property (nonatomic, copy, readonly) stream_scanner consumer;
+@property (nonatomic, copy, readonly) data_callback handler;
+@property (nonatomic, assign) size_t bytesNeeded;
+@property (nonatomic, assign, readonly) BOOL readToCurrentFrame;
+@property (nonatomic, assign, readonly) BOOL unmaskBytes;
+
+@end
+
+// This class is not thread-safe, and is expected to always be run on the same queue.
+@interface FSRIOConsumerPool : NSObject
+
+- (id)initWithBufferCapacity:(NSUInteger)poolSize;
+
+- (FSRIOConsumer *)consumerWithScanner:(stream_scanner)scanner handler:(data_callback)handler bytesNeeded:(size_t)bytesNeeded readToCurrentFrame:(BOOL)readToCurrentFrame unmaskBytes:(BOOL)unmaskBytes;
+- (void)returnConsumer:(FSRIOConsumer *)consumer;
+
+@end
+
+@interface FSRWebSocket () <NSStreamDelegate>
+
+- (void)_writeData:(NSData *)data;
+- (void)_closeWithProtocolError:(NSString *)message;
+- (void)_failWithError:(NSError *)error;
+
+- (void)_disconnect;
+
+- (void)_readFrameNew;
+- (void)_readFrameContinue;
+
+- (void)_pumpScanner;
+
+- (void)_pumpWriting;
+
+- (void)_addConsumerWithScanner:(stream_scanner)consumer callback:(data_callback)callback;
+- (void)_addConsumerWithDataLength:(size_t)dataLength callback:(data_callback)callback readToCurrentFrame:(BOOL)readToCurrentFrame unmaskBytes:(BOOL)unmaskBytes;
+- (void)_addConsumerWithScanner:(stream_scanner)consumer callback:(data_callback)callback dataLength:(size_t)dataLength;
+- (void)_readUntilBytes:(const void *)bytes length:(size_t)length callback:(data_callback)dataHandler;
+- (void)_readUntilHeaderCompleteWithCallback:(data_callback)dataHandler;
+
+- (void)_sendFrameWithOpcode:(FSROpCode)opcode data:(id)data;
+
+- (BOOL)_checkHandshake:(CFHTTPMessageRef)httpMessage;
+- (void)_SR_commonInit;
+
+- (void)_initializeStreams;
+- (void)_connect;
+
+@property (nonatomic) FSRReadyState readyState;
+
+@property (nonatomic) NSOperationQueue *delegateOperationQueue;
+@property (nonatomic) dispatch_queue_t delegateDispatchQueue;
+
+@end
+
+
+@implementation FSRWebSocket {
+ NSInteger _webSocketVersion;
+
+ NSOperationQueue *_delegateOperationQueue;
+ dispatch_queue_t _delegateDispatchQueue;
+ dispatch_queue_t _workQueue;
+ NSMutableArray *_consumers;
+
+ NSInputStream *_inputStream;
+ NSOutputStream *_outputStream;
+
+ NSMutableData *_readBuffer;
+ NSInteger _readBufferOffset;
+
+ NSMutableData *_outputBuffer;
+ NSInteger _outputBufferOffset;
+
+ uint8_t _currentFrameOpcode;
+ size_t _currentFrameCount;
+ size_t _readOpCount;
+ uint32_t _currentStringScanPosition;
+ NSMutableData *_currentFrameData;
+
+ NSString *_closeReason;
+
+ NSString *_secKey;
+
+ BOOL _pinnedCertFound;
+
+ uint8_t _currentReadMaskKey[4];
+ size_t _currentReadMaskOffset;
+
+ BOOL _consumerStopped;
+
+ BOOL _closeWhenFinishedWriting;
+ BOOL _failed;
+
+ BOOL _secure;
+ NSURLRequest *_urlRequest;
+ NSString *_userAgent;
+
+ CFHTTPMessageRef _receivedHTTPHeaders;
+
+ BOOL _sentClose;
+ BOOL _didFail;
+ BOOL _cleanupScheduled;
+ int _closeCode;
+
+ BOOL _isPumping;
+
+ NSMutableSet *_scheduledRunloops;
+
+ // We use this to retain ourselves.
+ __strong FSRWebSocket *_selfRetain;
+
+ NSArray *_requestedProtocols;
+ FSRIOConsumerPool *_consumerPool;
+}
+
+@synthesize delegate = _delegate;
+@synthesize url = _url;
+@synthesize readyState = _readyState;
+@synthesize protocol = _protocol;
+
+static __strong NSData *CRLFCRLF;
+
++ (void)initialize;
+{
+ CRLFCRLF = [[NSData alloc] initWithBytes:"\r\n\r\n" length:4];
+}
+
+- (id)initWithURLRequest:(NSURLRequest *)request protocols:(NSArray *)protocols queue:(dispatch_queue_t)queue andUserAgent:(NSString *)userAgent;
+{
+ self = [super init];
+ if (self) {
+ assert(request.URL);
+ _url = request.URL;
+ NSString *scheme = [_url scheme];
+
+ _requestedProtocols = [protocols copy];
+ _userAgent = userAgent;
+
+ assert([scheme isEqualToString:@"ws"] || [scheme isEqualToString:@"http"] || [scheme isEqualToString:@"wss"] || [scheme isEqualToString:@"https"]);
+ _urlRequest = request;
+
+ if ([scheme isEqualToString:@"wss"] || [scheme isEqualToString:@"https"]) {
+ _secure = YES;
+ }
+
+ if (!queue) {
+ _delegateDispatchQueue = dispatch_get_main_queue();
+ } else {
+ _delegateDispatchQueue = queue;
+ }
+
+ [self _SR_commonInit];
+ }
+
+ return self;
+}
+
+- (id)initWithURLRequest:(NSURLRequest *)request protocols:(NSArray *)protocols;
+{
+ return [self initWithURLRequest:request protocols:nil queue:nil andUserAgent:nil];
+}
+
+- (id)initWithURLRequest:(NSURLRequest *)request queue:(dispatch_queue_t)queue andUserAgent:(NSString *)userAgent;
+{
+ return [self initWithURLRequest:request protocols:nil queue:queue andUserAgent:userAgent];
+}
+
+- (id)initWithURLRequest:(NSURLRequest *)request;
+{
+ return [self initWithURLRequest:request protocols:nil];
+}
+
+- (id)initWithURL:(NSURL *)url;
+{
+ return [self initWithURL:url protocols:nil];
+}
+
+- (id)initWithURL:(NSURL *)url protocols:(NSArray *)protocols;
+{
+ NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url];
+ return [self initWithURLRequest:request protocols:protocols];
+}
+
+- (void)_SR_commonInit;
+{
+ _readyState = SR_CONNECTING;
+
+ _consumerStopped = YES;
+
+ _webSocketVersion = 13;
+
+ _workQueue = dispatch_queue_create(NULL, DISPATCH_QUEUE_SERIAL);
+
+ // Going to set a specific on the queue so we can validate we're on the work queue
+ dispatch_queue_set_specific(_workQueue, (__bridge void *)self, maybe_bridge(_workQueue), NULL);
+
+ sr_dispatch_retain(_delegateDispatchQueue);
+
+ _readBuffer = [[NSMutableData alloc] init];
+ _outputBuffer = [[NSMutableData alloc] init];
+
+ _currentFrameData = [[NSMutableData alloc] init];
+
+ _consumers = [[NSMutableArray alloc] init];
+
+ _consumerPool = [[FSRIOConsumerPool alloc] init];
+
+ _scheduledRunloops = [[NSMutableSet alloc] init];
+
+ [self _initializeStreams];
+
+ // default handlers
+}
+
+- (void)assertOnWorkQueue;
+{
+ assert(dispatch_get_specific((__bridge void *)self) == maybe_bridge(_workQueue));
+}
+
+- (void)dealloc
+{
+ _inputStream.delegate = nil;
+ _outputStream.delegate = nil;
+
+ [_inputStream close];
+ [_outputStream close];
+
+ sr_dispatch_release(_workQueue);
+ _workQueue = NULL;
+
+ if (_receivedHTTPHeaders) {
+ CFRelease(_receivedHTTPHeaders);
+ _receivedHTTPHeaders = NULL;
+ }
+
+ if (_delegateDispatchQueue) {
+ sr_dispatch_release(_delegateDispatchQueue);
+ _delegateDispatchQueue = NULL;
+ }
+}
+
+#ifndef NDEBUG
+
+- (void)setReadyState:(FSRReadyState)aReadyState;
+{
+ [self willChangeValueForKey:@"readyState"];
+ assert(aReadyState > _readyState);
+ _readyState = aReadyState;
+ [self didChangeValueForKey:@"readyState"];
+}
+
+#endif
+
+- (void)open;
+{
+ assert(_url);
+ NSAssert(_readyState == SR_CONNECTING, @"Cannot call -(void)open on SRWebSocket more than once");
+
+ _selfRetain = self;
+
+ [self _connect];
+}
+
+// Calls block on delegate queue
+- (void)_performDelegateBlock:(dispatch_block_t)block;
+{
+ if (_delegateOperationQueue) {
+ [_delegateOperationQueue addOperationWithBlock:block];
+ } else {
+ assert(_delegateDispatchQueue);
+ dispatch_async(_delegateDispatchQueue, block);
+ }
+}
+
+- (void)setDelegateDispatchQueue:(dispatch_queue_t)queue;
+{
+ if (queue) {
+ sr_dispatch_retain(queue);
+ }
+
+ if (_delegateDispatchQueue) {
+ sr_dispatch_release(_delegateDispatchQueue);
+ }
+
+ _delegateDispatchQueue = queue;
+}
+
+- (BOOL)_checkHandshake:(CFHTTPMessageRef)httpMessage;
+{
+ NSString *acceptHeader = CFBridgingRelease(CFHTTPMessageCopyHeaderFieldValue(httpMessage, CFSTR("Sec-WebSocket-Accept")));
+
+ if (acceptHeader == nil) {
+ return NO;
+ }
+
+ NSString *concattedString = [_secKey stringByAppendingString:SRWebSocketAppendToSecKeyString];
+ NSString *expectedAccept = [concattedString stringBySHA1ThenBase64Encoding];
+
+ return [acceptHeader isEqualToString:expectedAccept];
+}
+
+- (void)_HTTPHeadersDidFinish;
+{
+ NSInteger responseCode = CFHTTPMessageGetResponseStatusCode(_receivedHTTPHeaders);
+
+ if (responseCode >= 400) {
+ SRFastLog(@"Request failed with response code %d", responseCode);
+ [self _failWithError:[NSError errorWithDomain:@"org.lolrus.SocketRocket" code:2132 userInfo:[NSDictionary dictionaryWithObject:[NSString stringWithFormat:@"received bad response code from server %u", (int)responseCode] forKey:NSLocalizedDescriptionKey]]];
+ return;
+
+ }
+
+ if(![self _checkHandshake:_receivedHTTPHeaders]) {
+ [self _failWithError:[NSError errorWithDomain:FSRWebSocketErrorDomain code:2133 userInfo:[NSDictionary dictionaryWithObject:[NSString stringWithFormat:@"Invalid Sec-WebSocket-Accept response"] forKey:NSLocalizedDescriptionKey]]];
+ return;
+ }
+
+ NSString *negotiatedProtocol = CFBridgingRelease(CFHTTPMessageCopyHeaderFieldValue(_receivedHTTPHeaders, CFSTR("Sec-WebSocket-Protocol")));
+ if (negotiatedProtocol) {
+ // Make sure we requested the protocol
+ if ([_requestedProtocols indexOfObject:negotiatedProtocol] == NSNotFound) {
+ [self _failWithError:[NSError errorWithDomain:FSRWebSocketErrorDomain code:2133 userInfo:[NSDictionary dictionaryWithObject:[NSString stringWithFormat:@"Server specified Sec-WebSocket-Protocol that wasn't requested"] forKey:NSLocalizedDescriptionKey]]];
+ return;
+ }
+
+ _protocol = negotiatedProtocol;
+ }
+
+ self.readyState = SR_OPEN;
+
+ if (!_didFail) {
+ [self _readFrameNew];
+ }
+
+ [self _performDelegateBlock:^{
+ if ([self.delegate respondsToSelector:@selector(webSocketDidOpen:)]) {
+ [self.delegate webSocketDidOpen:self];
+ };
+ }];
+}
+
+
+- (void)_readHTTPHeader;
+{
+ if (_receivedHTTPHeaders == NULL) {
+ _receivedHTTPHeaders = CFHTTPMessageCreateEmpty(NULL, NO);
+ }
+
+ [self _readUntilHeaderCompleteWithCallback:^(FSRWebSocket *self, NSData *data) {
+ CFHTTPMessageAppendBytes(_receivedHTTPHeaders, (const UInt8 *)data.bytes, data.length);
+
+ if (CFHTTPMessageIsHeaderComplete(_receivedHTTPHeaders)) {
+ SRFastLog(@"Finished reading headers %@", CFBridgingRelease(CFHTTPMessageCopyAllHeaderFields(_receivedHTTPHeaders)));
+ [self _HTTPHeadersDidFinish];
+ } else {
+ [self _readHTTPHeader];
+ }
+ }];
+}
+
+- (void)didConnect
+{
+ SRFastLog(@"Connected");
+ CFHTTPMessageRef request = CFHTTPMessageCreateRequest(NULL, CFSTR("GET"), (__bridge CFURLRef)_url, kCFHTTPVersion1_1);
+
+ // Set host first so it defaults
+ CFHTTPMessageSetHeaderFieldValue(request, CFSTR("Host"), (__bridge CFStringRef)(_url.port ? [NSString stringWithFormat:@"%@:%@", _url.host, _url.port] : _url.host));
+
+ NSMutableData *keyBytes = [[NSMutableData alloc] initWithLength:16];
+ int result = SecRandomCopyBytes(kSecRandomDefault, keyBytes.length, keyBytes.mutableBytes);
+ assert(result == 0);
+ _secKey = [FSRUtilities base64EncodedStringFromData:keyBytes];
+ assert([_secKey length] == 24);
+
+ CFHTTPMessageSetHeaderFieldValue(request, CFSTR("Upgrade"), CFSTR("websocket"));
+ CFHTTPMessageSetHeaderFieldValue(request, CFSTR("Connection"), CFSTR("Upgrade"));
+ if (_userAgent) {
+ CFHTTPMessageSetHeaderFieldValue(request, CFSTR("User-Agent"), (__bridge CFStringRef)_userAgent);
+ }
+
+ CFHTTPMessageSetHeaderFieldValue(request, CFSTR("Sec-WebSocket-Key"), (__bridge CFStringRef)_secKey);
+ CFHTTPMessageSetHeaderFieldValue(request, CFSTR("Sec-WebSocket-Version"), (__bridge CFStringRef)[NSString stringWithFormat:@"%u", (int)_webSocketVersion]);
+
+ CFHTTPMessageSetHeaderFieldValue(request, CFSTR("Origin"), (__bridge CFStringRef)_url.SR_origin);
+
+ if (_requestedProtocols) {
+ CFHTTPMessageSetHeaderFieldValue(request, CFSTR("Sec-WebSocket-Protocol"), (__bridge CFStringRef)[_requestedProtocols componentsJoinedByString:@", "]);
+ }
+
+ [_urlRequest.allHTTPHeaderFields enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
+ CFHTTPMessageSetHeaderFieldValue(request, (__bridge CFStringRef)key, (__bridge CFStringRef)obj);
+ }];
+
+ NSData *message = CFBridgingRelease(CFHTTPMessageCopySerializedMessage(request));
+
+ CFRelease(request);
+
+ [self _writeData:message];
+ [self _readHTTPHeader];
+}
+
+//- (void)_connectToHost:(NSString *)host port:(NSInteger)port;
+- (void)_initializeStreams;
+{
+ NSInteger port = _url.port.integerValue;
+ if (port == 0) {
+ if (!_secure) {
+ port = 80;
+ } else {
+ port = 443;
+ }
+ }
+ NSString *host = _url.host;
+
+ CFReadStreamRef readStream = NULL;
+ CFWriteStreamRef writeStream = NULL;
+
+ CFStreamCreatePairWithSocketToHost(NULL, (__bridge CFStringRef)host, (int)port, &readStream, &writeStream);
+
+ // XXX
+ CFReadStreamSetProperty(readStream, kCFStreamNetworkServiceType, kCFStreamNetworkServiceTypeBackground);
+ CFWriteStreamSetProperty(writeStream, kCFStreamNetworkServiceType, kCFStreamNetworkServiceTypeBackground);
+
+ _outputStream = CFBridgingRelease(writeStream);
+ _inputStream = CFBridgingRelease(readStream);
+
+
+ if (_secure) {
+ NSMutableDictionary *SSLOptions = [[NSMutableDictionary alloc] init];
+
+ [_outputStream setProperty:(__bridge id)kCFStreamSocketSecurityLevelNegotiatedSSL forKey:(__bridge id)kCFStreamPropertySocketSecurityLevel];
+
+ // If we're using pinned certs, don't validate the certificate chain
+ if ([_urlRequest FSR_SSLPinnedCertificates].count) {
+ [SSLOptions setValue:[NSNumber numberWithBool:NO] forKey:(__bridge id)kCFStreamSSLValidatesCertificateChain];
+ }
+
+ [_outputStream setProperty:SSLOptions
+ forKey:(__bridge id)kCFStreamPropertySSLSettings];
+ }
+
+ _inputStream.delegate = self;
+ _outputStream.delegate = self;
+
+ [_outputStream open];
+ [_inputStream open];
+}
+
+- (void)_connect;
+{
+ if (!_scheduledRunloops.count) {
+ [self scheduleInRunLoop:[NSRunLoop FSR_networkRunLoop] forMode:NSDefaultRunLoopMode];
+ }
+
+
+ [_outputStream open];
+ [_inputStream open];
+}
+
+- (void)scheduleInRunLoop:(NSRunLoop *)aRunLoop forMode:(NSString *)mode;
+{
+ [_outputStream scheduleInRunLoop:aRunLoop forMode:mode];
+ [_inputStream scheduleInRunLoop:aRunLoop forMode:mode];
+
+ [_scheduledRunloops addObject:@[aRunLoop, mode]];
+}
+
+- (void)unscheduleFromRunLoop:(NSRunLoop *)aRunLoop forMode:(NSString *)mode;
+{
+ [_outputStream removeFromRunLoop:aRunLoop forMode:mode];
+ [_inputStream removeFromRunLoop:aRunLoop forMode:mode];
+
+ [_scheduledRunloops removeObject:@[aRunLoop, mode]];
+}
+
+- (void)close;
+{
+ [self closeWithCode:-1 reason:nil];
+}
+
+- (void)closeWithCode:(NSInteger)code reason:(NSString *)reason;
+{
+ assert(code);
+ dispatch_async(_workQueue, ^{
+ if (self.readyState == SR_CLOSING || self.readyState == SR_CLOSED) {
+ return;
+ }
+
+ BOOL wasConnecting = self.readyState == SR_CONNECTING;
+
+ self.readyState = SR_CLOSING;
+
+ SRFastLog(@"Closing with code %d reason %@", code, reason);
+
+ if (wasConnecting) {
+ [self _disconnect];
+ return;
+ }
+
+ size_t maxMsgSize = [reason maximumLengthOfBytesUsingEncoding:NSUTF8StringEncoding];
+ NSMutableData *mutablePayload = [[NSMutableData alloc] initWithLength:sizeof(uint16_t) + maxMsgSize];
+ NSData *payload = mutablePayload;
+
+ ((uint16_t *)mutablePayload.mutableBytes)[0] = EndianU16_BtoN(code);
+
+ if (reason) {
+ NSRange remainingRange = {0};
+
+ NSUInteger usedLength = 0;
+
+ BOOL success = [reason getBytes:(char *)mutablePayload.mutableBytes + sizeof(uint16_t) maxLength:payload.length - sizeof(uint16_t) usedLength:&usedLength encoding:NSUTF8StringEncoding options:NSStringEncodingConversionExternalRepresentation range:NSMakeRange(0, reason.length) remainingRange:&remainingRange];
+
+ assert(success);
+ assert(remainingRange.length == 0);
+
+ if (usedLength != maxMsgSize) {
+ payload = [payload subdataWithRange:NSMakeRange(0, usedLength + sizeof(uint16_t))];
+ }
+ }
+
+
+ [self _sendFrameWithOpcode:SROpCodeConnectionClose data:payload];
+ });
+}
+
+- (void)_closeWithProtocolError:(NSString *)message;
+{
+ // Need to shunt this on the _callbackQueue first to see if they received any messages
+ [self _performDelegateBlock:^{
+ [self closeWithCode:SRStatusCodeProtocolError reason:message];
+ dispatch_async(_workQueue, ^{
+ [self _disconnect];
+ });
+ }];
+}
+
+- (void)_failWithError:(NSError *)error;
+{
+ dispatch_async(_workQueue, ^{
+ if (self.readyState != SR_CLOSED) {
+ _failed = YES;
+ [self _performDelegateBlock:^{
+ if ([self.delegate respondsToSelector:@selector(webSocket:didFailWithError:)]) {
+ [self.delegate webSocket:self didFailWithError:error];
+ }
+ }];
+
+ self.readyState = SR_CLOSED;
+
+ SRFastLog(@"Failing with error %@", error.localizedDescription);
+
+ [self _disconnect];
+ [self _scheduleCleanup];
+ }
+ });
+}
+
+- (void)_writeData:(NSData *)data;
+{
+ [self assertOnWorkQueue];
+
+ if (_closeWhenFinishedWriting) {
+ return;
+ }
+ [_outputBuffer appendData:data];
+ [self _pumpWriting];
+}
+- (void)send:(id)data;
+{
+ SRFastLog(@"Sending data %@", data);
+ NSAssert(self.readyState != SR_CONNECTING, @"Invalid State: Cannot call send: until connection is open");
+ // TODO: maybe not copy this for performance
+ data = [data copy];
+ dispatch_async(_workQueue, ^{
+ if ([data isKindOfClass:[NSString class]]) {
+ [self _sendFrameWithOpcode:SROpCodeTextFrame data:[(NSString *)data dataUsingEncoding:NSUTF8StringEncoding]];
+ } else if ([data isKindOfClass:[NSData class]]) {
+ [self _sendFrameWithOpcode:SROpCodeBinaryFrame data:data];
+ } else if (data == nil) {
+ [self _sendFrameWithOpcode:SROpCodeTextFrame data:data];
+ } else {
+ assert(NO);
+ }
+ });
+}
+
+- (void)handlePing:(NSData *)pingData;
+{
+ // Need to pingpong this off _callbackQueue first to make sure messages happen in order
+ [self _performDelegateBlock:^{
+ dispatch_async(_workQueue, ^{
+ [self _sendFrameWithOpcode:SROpCodePong data:pingData];
+ });
+ }];
+}
+
+- (void)handlePong;
+{
+ // NOOP
+}
+
+- (void)_handleMessage:(id)message
+{
+ SRFastLog(@"Received message");
+ [self _performDelegateBlock:^{
+ if ([self.delegate respondsToSelector:@selector(webSocket:didReceiveMessage:)]) {
+ [self.delegate webSocket:self didReceiveMessage:message];
+ }
+ }];
+}
+
+
+static inline BOOL closeCodeIsValid(int closeCode) {
+ if (closeCode < 1000) {
+ return NO;
+ }
+
+ if (closeCode >= 1000 && closeCode <= 1011) {
+ if (closeCode == 1004 ||
+ closeCode == 1005 ||
+ closeCode == 1006) {
+ return NO;
+ }
+ return YES;
+ }
+
+ if (closeCode >= 3000 && closeCode <= 3999) {
+ return YES;
+ }
+
+ if (closeCode >= 4000 && closeCode <= 4999) {
+ return YES;
+ }
+
+ return NO;
+}
+
+// Note from RFC:
+//
+// If there is a body, the first two
+// bytes of the body MUST be a 2-byte unsigned integer (in network byte
+// order) representing a status code with value /code/ defined in
+// Section 7.4. Following the 2-byte integer the body MAY contain UTF-8
+// encoded data with value /reason/, the interpretation of which is not
+// defined by this specification.
+
+- (void)handleCloseWithData:(NSData *)data;
+{
+ size_t dataSize = data.length;
+ __block uint16_t closeCode = 0;
+
+ SRFastLog(@"Received close frame");
+
+ if (dataSize == 1) {
+ // TODO handle error
+ [self _closeWithProtocolError:@"Payload for close must be larger than 2 bytes"];
+ return;
+ } else if (dataSize >= 2) {
+ [data getBytes:&closeCode length:sizeof(closeCode)];
+ _closeCode = EndianU16_BtoN(closeCode);
+ if (!closeCodeIsValid(_closeCode)) {
+ [self _closeWithProtocolError:[NSString stringWithFormat:@"Cannot have close code of %d", _closeCode]];
+ return;
+ }
+ if (dataSize > 2) {
+ _closeReason = [[NSString alloc] initWithData:[data subdataWithRange:NSMakeRange(2, dataSize - 2)] encoding:NSUTF8StringEncoding];
+ if (!_closeReason) {
+ [self _closeWithProtocolError:@"Close reason MUST be valid UTF-8"];
+ return;
+ }
+ }
+ } else {
+ _closeCode = SRStatusNoStatusReceived;
+ }
+
+ [self assertOnWorkQueue];
+
+ if (self.readyState == SR_OPEN) {
+ [self closeWithCode:1000 reason:nil];
+ }
+ dispatch_async(_workQueue, ^{
+ [self _disconnect];
+ });
+}
+
+- (void)_disconnect;
+{
+ [self assertOnWorkQueue];
+ SRFastLog(@"Trying to disconnect");
+ _closeWhenFinishedWriting = YES;
+ [self _pumpWriting];
+}
+
+- (void)_handleFrameWithData:(NSData *)frameData opCode:(NSInteger)opcode;
+{
+ // Check that the current data is valid UTF8
+
+ BOOL isControlFrame = (opcode == SROpCodePing || opcode == SROpCodePong || opcode == SROpCodeConnectionClose);
+ if (!isControlFrame) {
+ [self _readFrameNew];
+ } else {
+ dispatch_async(_workQueue, ^{
+ [self _readFrameContinue];
+ });
+ }
+
+ switch (opcode) {
+ case SROpCodeTextFrame: {
+ NSString *str = [[NSString alloc] initWithData:frameData encoding:NSUTF8StringEncoding];
+ if (str == nil && frameData) {
+ [self closeWithCode:SRStatusCodeInvalidUTF8 reason:@"Text frames must be valid UTF-8"];
+ dispatch_async(_workQueue, ^{
+ [self _disconnect];
+ });
+
+ return;
+ }
+ [self _handleMessage:str];
+ break;
+ }
+ case SROpCodeBinaryFrame:
+ [self _handleMessage:[frameData copy]];
+ break;
+ case SROpCodeConnectionClose:
+ [self handleCloseWithData:frameData];
+ break;
+ case SROpCodePing:
+ [self handlePing:frameData];
+ break;
+ case SROpCodePong:
+ [self handlePong];
+ break;
+ default:
+ [self _closeWithProtocolError:[NSString stringWithFormat:@"Unknown opcode %u", (int)opcode]];
+ // TODO: Handle invalid opcode
+ break;
+ }
+}
+
+- (void)_handleFrameHeader:(frame_header)frame_header curData:(NSData *)curData;
+{
+ assert(frame_header.opcode != 0);
+
+ if (self.readyState != SR_OPEN) {
+ return;
+ }
+
+
+ BOOL isControlFrame = (frame_header.opcode == SROpCodePing || frame_header.opcode == SROpCodePong || frame_header.opcode == SROpCodeConnectionClose);
+
+ if (isControlFrame && !frame_header.fin) {
+ [self _closeWithProtocolError:@"Fragmented control frames not allowed"];
+ return;
+ }
+
+ if (isControlFrame && frame_header.payload_length >= 126) {
+ [self _closeWithProtocolError:@"Control frames cannot have payloads larger than 126 bytes"];
+ return;
+ }
+
+ if (!isControlFrame) {
+ _currentFrameOpcode = frame_header.opcode;
+ _currentFrameCount += 1;
+ }
+
+ if (frame_header.payload_length == 0) {
+ if (isControlFrame) {
+ [self _handleFrameWithData:curData opCode:frame_header.opcode];
+ } else {
+ if (frame_header.fin) {
+ [self _handleFrameWithData:_currentFrameData opCode:frame_header.opcode];
+ } else {
+ // TODO add assert that opcode is not a control;
+ [self _readFrameContinue];
+ }
+ }
+ } else {
+ [self _addConsumerWithDataLength:(size_t)frame_header.payload_length callback:^(FSRWebSocket *self, NSData *newData) {
+ if (isControlFrame) {
+ [self _handleFrameWithData:newData opCode:frame_header.opcode];
+ } else {
+ if (frame_header.fin) {
+ [self _handleFrameWithData:self->_currentFrameData opCode:frame_header.opcode];
+ } else {
+ // TODO add assert that opcode is not a control;
+ [self _readFrameContinue];
+ }
+
+ }
+ } readToCurrentFrame:!isControlFrame unmaskBytes:frame_header.masked];
+ }
+}
+
+/* From RFC:
+
+ 0 1 2 3
+ 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+ +-+-+-+-+-------+-+-------------+-------------------------------+
+ |F|R|R|R| opcode|M| Payload len | Extended payload length |
+ |I|S|S|S| (4) |A| (7) | (16/64) |
+ |N|V|V|V| |S| | (if payload len==126/127) |
+ | |1|2|3| |K| | |
+ +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
+ | Extended payload length continued, if payload len == 127 |
+ + - - - - - - - - - - - - - - - +-------------------------------+
+ | |Masking-key, if MASK set to 1 |
+ +-------------------------------+-------------------------------+
+ | Masking-key (continued) | Payload Data |
+ +-------------------------------- - - - - - - - - - - - - - - - +
+ : Payload Data continued ... :
+ + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
+ | Payload Data continued ... |
+ +---------------------------------------------------------------+
+ */
+
+static const uint8_t SRFinMask = 0x80;
+static const uint8_t SROpCodeMask = 0x0F;
+static const uint8_t SRRsvMask = 0x70;
+static const uint8_t SRMaskMask = 0x80;
+static const uint8_t SRPayloadLenMask = 0x7F;
+
+
+- (void)_readFrameContinue;
+{
+ assert((_currentFrameCount == 0 && _currentFrameOpcode == 0) || (_currentFrameCount > 0 && _currentFrameOpcode > 0));
+
+ [self _addConsumerWithDataLength:2 callback:^(FSRWebSocket *self, NSData *data) {
+ __block frame_header header = {0};
+
+ const uint8_t *headerBuffer = data.bytes;
+ assert(data.length >= 2);
+
+ if (headerBuffer[0] & SRRsvMask) {
+ [self _closeWithProtocolError:@"Server used RSV bits"];
+ return;
+ }
+
+ uint8_t receivedOpcode = (SROpCodeMask & headerBuffer[0]);
+
+ BOOL isControlFrame = (receivedOpcode == SROpCodePing || receivedOpcode == SROpCodePong || receivedOpcode == SROpCodeConnectionClose);
+
+ if (!isControlFrame && receivedOpcode != 0 && self->_currentFrameCount > 0) {
+ [self _closeWithProtocolError:@"all data frames after the initial data frame must have opcode 0"];
+ return;
+ }
+
+ if (receivedOpcode == 0 && self->_currentFrameCount == 0) {
+ [self _closeWithProtocolError:@"cannot continue a message"];
+ return;
+ }
+
+ header.opcode = receivedOpcode == 0 ? self->_currentFrameOpcode : receivedOpcode;
+
+ header.fin = !!(SRFinMask & headerBuffer[0]);
+
+
+ header.masked = !!(SRMaskMask & headerBuffer[1]);
+ header.payload_length = SRPayloadLenMask & headerBuffer[1];
+
+ headerBuffer = NULL;
+
+ if (header.masked) {
+ [self _closeWithProtocolError:@"Client must receive unmasked data"];
+ }
+
+ size_t extra_bytes_needed = header.masked ? sizeof(_currentReadMaskKey) : 0;
+
+ if (header.payload_length == 126) {
+ extra_bytes_needed += sizeof(uint16_t);
+ } else if (header.payload_length == 127) {
+ extra_bytes_needed += sizeof(uint64_t);
+ }
+
+ if (extra_bytes_needed == 0) {
+ [self _handleFrameHeader:header curData:self->_currentFrameData];
+ } else {
+ [self _addConsumerWithDataLength:extra_bytes_needed callback:^(FSRWebSocket *self, NSData *data) {
+ size_t mapped_size = data.length;
+ const void *mapped_buffer = data.bytes;
+ size_t offset = 0;
+
+ if (header.payload_length == 126) {
+ assert(mapped_size >= sizeof(uint16_t));
+ uint16_t newLen = EndianU16_BtoN(*(uint16_t *)(mapped_buffer));
+ header.payload_length = newLen;
+ offset += sizeof(uint16_t);
+ } else if (header.payload_length == 127) {
+ assert(mapped_size >= sizeof(uint64_t));
+ header.payload_length = EndianU64_BtoN(*(uint64_t *)(mapped_buffer));
+ offset += sizeof(uint64_t);
+ } else {
+ assert(header.payload_length < 126 && header.payload_length >= 0);
+ }
+
+
+ if (header.masked) {
+ assert(mapped_size >= sizeof(_currentReadMaskOffset) + offset);
+ memcpy(self->_currentReadMaskKey, ((uint8_t *)mapped_buffer) + offset, sizeof(self->_currentReadMaskKey));
+ }
+
+ [self _handleFrameHeader:header curData:self->_currentFrameData];
+ } readToCurrentFrame:NO unmaskBytes:NO];
+ }
+ } readToCurrentFrame:NO unmaskBytes:NO];
+}
+
+- (void)_readFrameNew;
+{
+ dispatch_async(_workQueue, ^{
+ [_currentFrameData setLength:0];
+
+ _currentFrameOpcode = 0;
+ _currentFrameCount = 0;
+ _readOpCount = 0;
+ _currentStringScanPosition = 0;
+
+ [self _readFrameContinue];
+ });
+}
+
+- (void)_pumpWriting;
+{
+ [self assertOnWorkQueue];
+
+ NSUInteger dataLength = _outputBuffer.length;
+ if (dataLength - _outputBufferOffset > 0 && _outputStream.hasSpaceAvailable) {
+ NSUInteger bytesWritten = [_outputStream write:_outputBuffer.bytes + _outputBufferOffset maxLength:dataLength - _outputBufferOffset];
+ if (bytesWritten == -1) {
+ [self _failWithError:[NSError errorWithDomain:@"org.lolrus.SocketRocket" code:2145 userInfo:[NSDictionary dictionaryWithObject:@"Error writing to stream" forKey:NSLocalizedDescriptionKey]]];
+ return;
+ }
+
+ _outputBufferOffset += bytesWritten;
+
+ if (_outputBufferOffset > 4096 && _outputBufferOffset > (_outputBuffer.length >> 1)) {
+ _outputBuffer = [[NSMutableData alloc] initWithBytes:(char *)_outputBuffer.bytes + _outputBufferOffset length:_outputBuffer.length - _outputBufferOffset];
+ _outputBufferOffset = 0;
+ }
+ }
+
+ if (_closeWhenFinishedWriting &&
+ _outputBuffer.length - _outputBufferOffset == 0 &&
+ (_inputStream.streamStatus != NSStreamStatusNotOpen &&
+ _inputStream.streamStatus != NSStreamStatusClosed) &&
+ !_sentClose) {
+ _sentClose = YES;
+
+ @synchronized (self) {
+ [_outputStream close];
+ [_inputStream close];
+
+ // TODO: Why are we missing the SocketRocket code to call unscheduleFromRunLoop???
+ }
+
+ if (!_failed) {
+ [self _performDelegateBlock:^{
+ if ([self.delegate respondsToSelector:@selector(webSocket:didCloseWithCode:reason:wasClean:)]) {
+ [self.delegate webSocket:self didCloseWithCode:_closeCode reason:_closeReason wasClean:YES];
+ }
+ }];
+ }
+ [self _scheduleCleanup];
+ }
+}
+
+- (void)_addConsumerWithScanner:(stream_scanner)consumer callback:(data_callback)callback;
+{
+ [self assertOnWorkQueue];
+ [self _addConsumerWithScanner:consumer callback:callback dataLength:0];
+}
+
+- (void)_addConsumerWithDataLength:(size_t)dataLength callback:(data_callback)callback readToCurrentFrame:(BOOL)readToCurrentFrame unmaskBytes:(BOOL)unmaskBytes;
+{
+ [self assertOnWorkQueue];
+ assert(dataLength);
+
+ [_consumers addObject:[_consumerPool consumerWithScanner:nil handler:callback bytesNeeded:dataLength readToCurrentFrame:readToCurrentFrame unmaskBytes:unmaskBytes]];
+ [self _pumpScanner];
+}
+
+- (void)_addConsumerWithScanner:(stream_scanner)consumer callback:(data_callback)callback dataLength:(size_t)dataLength;
+{
+ [self assertOnWorkQueue];
+ [_consumers addObject:[_consumerPool consumerWithScanner:consumer handler:callback bytesNeeded:dataLength readToCurrentFrame:NO unmaskBytes:NO]];
+ [self _pumpScanner];
+}
+
+
+- (void)_scheduleCleanup
+{
+ @synchronized(self) {
+ if (_cleanupScheduled) {
+ return;
+ }
+
+ _cleanupScheduled = YES;
+
+ // Cleanup NSStream delegate's in the same RunLoop used by the streams themselves:
+ // This way we'll prevent race conditions between handleEvent and SRWebsocket's dealloc
+ NSTimer *timer = [NSTimer timerWithTimeInterval:(0.0f) target:self selector:@selector(_cleanupSelfReference:) userInfo:nil repeats:NO];
+ [[NSRunLoop FSR_networkRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
+ }
+}
+
+- (void)_cleanupSelfReference:(NSTimer *)timer
+{
+ @synchronized(self) {
+ // Nuke NSStream delegate's
+ _inputStream.delegate = nil;
+ _outputStream.delegate = nil;
+
+ // Remove the streams, right now, from the networkRunLoop
+ [_inputStream close];
+ [_outputStream close];
+ }
+
+ // Cleanup selfRetain in the same GCD queue as usual
+ dispatch_async(_workQueue, ^{
+ _selfRetain = nil;
+ });
+}
+
+
+static const char CRLFCRLFBytes[] = {'\r', '\n', '\r', '\n'};
+
+- (void)_readUntilHeaderCompleteWithCallback:(data_callback)dataHandler;
+{
+ [self _readUntilBytes:CRLFCRLFBytes length:sizeof(CRLFCRLFBytes) callback:dataHandler];
+}
+
+- (void)_readUntilBytes:(const void *)bytes length:(size_t)length callback:(data_callback)dataHandler;
+{
+ // TODO optimize so this can continue from where we last searched
+ stream_scanner consumer = ^size_t(NSData *data) {
+ __block size_t found_size = 0;
+ __block size_t match_count = 0;
+
+ size_t size = data.length;
+ const unsigned char *buffer = data.bytes;
+ for (int i = 0; i < size; i++ ) {
+ if (((const unsigned char *)buffer)[i] == ((const unsigned char *)bytes)[match_count]) {
+ match_count += 1;
+ if (match_count == length) {
+ found_size = i + 1;
+ break;
+ }
+ } else {
+ match_count = 0;
+ }
+ }
+ return found_size;
+ };
+ [self _addConsumerWithScanner:consumer callback:dataHandler];
+}
+
+
+// Returns true if did work
+- (BOOL)_innerPumpScanner {
+
+ BOOL didWork = NO;
+
+ if (self.readyState >= SR_CLOSING) {
+ return didWork;
+ }
+
+ if (!_consumers.count) {
+ return didWork;
+ }
+
+ size_t curSize = _readBuffer.length - _readBufferOffset;
+ if (!curSize) {
+ return didWork;
+ }
+
+ FSRIOConsumer *consumer = [_consumers objectAtIndex:0];
+
+ size_t bytesNeeded = consumer.bytesNeeded;
+
+ size_t foundSize = 0;
+ if (consumer.consumer) {
+ NSData *tempView = [NSData dataWithBytesNoCopy:(char *)_readBuffer.bytes + _readBufferOffset length:_readBuffer.length - _readBufferOffset freeWhenDone:NO];
+ foundSize = consumer.consumer(tempView);
+ } else {
+ assert(consumer.bytesNeeded);
+ if (curSize >= bytesNeeded) {
+ foundSize = bytesNeeded;
+ } else if (consumer.readToCurrentFrame) {
+ foundSize = curSize;
+ }
+ }
+
+ NSData *slice = nil;
+ if (consumer.readToCurrentFrame || foundSize) {
+ NSRange sliceRange = NSMakeRange(_readBufferOffset, foundSize);
+ slice = [_readBuffer subdataWithRange:sliceRange];
+
+ _readBufferOffset += foundSize;
+
+ if (_readBufferOffset > 4096 && _readBufferOffset > (_readBuffer.length >> 1)) {
+ _readBuffer = [[NSMutableData alloc] initWithBytes:(char *)_readBuffer.bytes + _readBufferOffset length:_readBuffer.length - _readBufferOffset]; _readBufferOffset = 0;
+ }
+
+ if (consumer.unmaskBytes) {
+ NSMutableData *mutableSlice = [slice mutableCopy];
+
+ NSUInteger len = mutableSlice.length;
+ uint8_t *bytes = mutableSlice.mutableBytes;
+
+ for (int i = 0; i < len; i++) {
+ bytes[i] = bytes[i] ^ _currentReadMaskKey[_currentReadMaskOffset % sizeof(_currentReadMaskKey)];
+ _currentReadMaskOffset += 1;
+ }
+
+ slice = mutableSlice;
+ }
+
+ if (consumer.readToCurrentFrame) {
+ [_currentFrameData appendData:slice];
+
+ _readOpCount += 1;
+
+ if (_currentFrameOpcode == SROpCodeTextFrame) {
+ // Validate UTF8 stuff.
+ size_t currentDataSize = _currentFrameData.length;
+ if (_currentFrameOpcode == SROpCodeTextFrame && currentDataSize > 0) {
+ // TODO: Optimize the crap out of this. Don't really have to copy all the data each time
+
+ size_t scanSize = currentDataSize - _currentStringScanPosition;
+
+ NSData *scan_data = [_currentFrameData subdataWithRange:NSMakeRange(_currentStringScanPosition, scanSize)];
+ int32_t valid_utf8_size = validate_dispatch_data_partial_string(scan_data);
+
+ if (valid_utf8_size == -1) {
+ [self closeWithCode:SRStatusCodeInvalidUTF8 reason:@"Text frames must be valid UTF-8"];
+ dispatch_async(_workQueue, ^{
+ [self _disconnect];
+ });
+ return didWork;
+ } else {
+ _currentStringScanPosition += valid_utf8_size;
+ }
+ }
+
+ }
+
+ consumer.bytesNeeded -= foundSize;
+
+ if (consumer.bytesNeeded == 0) {
+ [_consumers removeObjectAtIndex:0];
+ consumer.handler(self, nil);
+ didWork = YES;
+ }
+ } else if (foundSize) {
+ [_consumers removeObjectAtIndex:0];
+ consumer.handler(self, slice);
+ didWork = YES;
+ }
+ }
+ return didWork;
+}
+
+-(void)_pumpScanner;
+{
+ [self assertOnWorkQueue];
+
+ if (!_isPumping) {
+ _isPumping = YES;
+ } else {
+ return;
+ }
+
+ while ([self _innerPumpScanner]) {
+
+ }
+
+ _isPumping = NO;
+}
+
+//#define NOMASK
+
+static const size_t SRFrameHeaderOverhead = 32;
+
+- (void)_sendFrameWithOpcode:(FSROpCode)opcode data:(id)data;
+{
+ [self assertOnWorkQueue];
+
+ NSAssert(data == nil || [data isKindOfClass:[NSData class]] || [data isKindOfClass:[NSString class]], @"Function expects nil, NSString or NSData");
+
+ size_t payloadLength = [data isKindOfClass:[NSString class]] ? [(NSString *)data lengthOfBytesUsingEncoding:NSUTF8StringEncoding] : [data length];
+
+ NSMutableData *frame = [[NSMutableData alloc] initWithLength:payloadLength + SRFrameHeaderOverhead];
+ if (!frame) {
+ [self closeWithCode:SRStatusCodeMessageTooBig reason:@"Message too big"];
+ return;
+ }
+ uint8_t *frame_buffer = (uint8_t *)[frame mutableBytes];
+
+ // set fin
+ frame_buffer[0] = SRFinMask | opcode;
+
+ BOOL useMask = YES;
+#ifdef NOMASK
+ useMask = NO;
+#endif
+
+ if (useMask) {
+ // set the mask and header
+ frame_buffer[1] |= SRMaskMask;
+ }
+
+ size_t frame_buffer_size = 2;
+
+ const uint8_t *unmasked_payload = NULL;
+ if ([data isKindOfClass:[NSData class]]) {
+ unmasked_payload = (uint8_t *)[data bytes];
+ } else if ([data isKindOfClass:[NSString class]]) {
+ unmasked_payload = (const uint8_t *)[data UTF8String];
+ } else {
+ assert(NO);
+ }
+
+ if (payloadLength < 126) {
+ frame_buffer[1] |= payloadLength;
+ } else if (payloadLength <= UINT16_MAX) {
+ frame_buffer[1] |= 126;
+ *((uint16_t *)(frame_buffer + frame_buffer_size)) = EndianU16_BtoN((uint16_t)payloadLength);
+ frame_buffer_size += sizeof(uint16_t);
+ } else {
+ frame_buffer[1] |= 127;
+ *((uint64_t *)(frame_buffer + frame_buffer_size)) = EndianU64_BtoN((uint64_t)payloadLength);
+ frame_buffer_size += sizeof(uint64_t);
+ }
+
+ if (!useMask) {
+ for (int i = 0; i < payloadLength; i++) {
+ frame_buffer[frame_buffer_size] = unmasked_payload[i];
+ frame_buffer_size += 1;
+ }
+ } else {
+ uint8_t *mask_key = frame_buffer + frame_buffer_size;
+ int result = SecRandomCopyBytes(kSecRandomDefault, sizeof(uint32_t), (uint8_t *)mask_key);
+ assert(result == 0);
+ frame_buffer_size += sizeof(uint32_t);
+
+ // TODO: could probably optimize this with SIMD
+ for (int i = 0; i < payloadLength; i++) {
+ frame_buffer[frame_buffer_size] = unmasked_payload[i] ^ mask_key[i % sizeof(uint32_t)];
+ frame_buffer_size += 1;
+ }
+ }
+
+ assert(frame_buffer_size <= [frame length]);
+ frame.length = frame_buffer_size;
+
+ [self _writeData:frame];
+}
+
+- (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode;
+{
+ __weak __typeof__(self) weakSelf = self;
+
+ // turn on keep-alive for the output stream.
+ if (eventCode == NSStreamEventOpenCompleted && aStream == _outputStream) {
+ CFDataRef socketData = CFWriteStreamCopyProperty((CFWriteStreamRef)_outputStream, kCFStreamPropertySocketNativeHandle);
+ // In rare cases socketData might be nil (there are crash reports out there), in which case we'll have to just
+ // live without keep-alive :(
+ if (socketData != nil) {
+ CFSocketNativeHandle socket;
+ CFDataGetBytes(socketData, CFRangeMake(0, sizeof(CFSocketNativeHandle)), (UInt8 *)&socket);
+ CFRelease(socketData);
+
+ int keepAliveOn = 1;
+ if (setsockopt(socket, SOL_SOCKET, SO_KEEPALIVE, &keepAliveOn, sizeof(keepAliveOn)) == -1) {
+ SRFastLog(@"Failed to turn on TCP keepalive for websocket");
+ }
+ }
+ }
+
+ if (_secure && !_pinnedCertFound && (eventCode == NSStreamEventHasBytesAvailable || eventCode == NSStreamEventHasSpaceAvailable)) {
+
+ NSArray *sslCerts = [_urlRequest FSR_SSLPinnedCertificates];
+ if (sslCerts) {
+ SecTrustRef secTrust = (__bridge SecTrustRef)[aStream propertyForKey:(__bridge id)kCFStreamPropertySSLPeerTrust];
+ if (secTrust) {
+ NSInteger numCerts = SecTrustGetCertificateCount(secTrust);
+ for (NSInteger i = 0; i < numCerts && !_pinnedCertFound; i++) {
+ SecCertificateRef cert = SecTrustGetCertificateAtIndex(secTrust, i);
+ NSData *certData = CFBridgingRelease(SecCertificateCopyData(cert));
+
+ for (id ref in sslCerts) {
+ SecCertificateRef trustedCert = (__bridge SecCertificateRef)ref;
+ NSData *trustedCertData = CFBridgingRelease(SecCertificateCopyData(trustedCert));
+
+ if ([trustedCertData isEqualToData:certData]) {
+ _pinnedCertFound = YES;
+ break;
+ }
+ }
+ }
+ }
+
+ if (!_pinnedCertFound) {
+ dispatch_async(_workQueue, ^{
+ NSDictionary *userInfo = @{ NSLocalizedDescriptionKey : @"Invalid server cert" };
+ [weakSelf _failWithError:[NSError errorWithDomain:@"org.lolrus.SocketRocket" code:23556 userInfo:userInfo]];
+ });
+ return;
+ }
+ }
+ }
+
+ // SRFastLog(@"%@ Got stream event %d", aStream, eventCode);
+ dispatch_async(_workQueue, ^{
+ [weakSelf safeHandleEvent:eventCode stream:aStream];
+ });
+}
+
+- (void)safeHandleEvent:(NSStreamEvent)eventCode stream:(NSStream *)aStream
+{
+ switch (eventCode) {
+ case NSStreamEventOpenCompleted: {
+ SRFastLog(@"NSStreamEventOpenCompleted %@", aStream);
+ if (self.readyState >= SR_CLOSING) {
+ return;
+ }
+
+
+ assert(_readBuffer);
+
+ if (self.readyState == SR_CONNECTING && aStream == _inputStream) {
+ [self didConnect];
+ }
+ [self _pumpWriting];
+ [self _pumpScanner];
+ break;
+ }
+
+ case NSStreamEventErrorOccurred: {
+ SRFastLog(@"NSStreamEventErrorOccurred %@ %@", aStream, [[aStream streamError] copy]);
+ /// TODO specify error better!
+ [self _failWithError:aStream.streamError];
+ _readBufferOffset = 0;
+ [_readBuffer setLength:0];
+ break;
+
+ }
+
+ case NSStreamEventEndEncountered: {
+ [self _pumpScanner];
+ SRFastLog(@"NSStreamEventEndEncountered %@", aStream);
+ if (aStream.streamError) {
+ [self _failWithError:aStream.streamError];
+ } else {
+ dispatch_async(_workQueue, ^{
+ if (self.readyState != SR_CLOSED) {
+ self.readyState = SR_CLOSED;
+ [self _scheduleCleanup];
+ }
+
+ if (!_sentClose && !_failed) {
+ _sentClose = YES;
+ // If we get closed in this state it's probably not clean because we should be sending this when we send messages
+ [self _performDelegateBlock:^{
+ if ([self.delegate respondsToSelector:@selector(webSocket:didCloseWithCode:reason:wasClean:)]) {
+ [self.delegate webSocket:self didCloseWithCode:0 reason:@"Stream end encountered" wasClean:NO];
+ }
+ }];
+ }
+ });
+ }
+
+ break;
+ }
+
+ case NSStreamEventHasBytesAvailable: {
+ SRFastLog(@"NSStreamEventHasBytesAvailable %@", aStream);
+ const NSUInteger bufferSize = 2048;
+ uint8_t buffer[bufferSize];
+
+ while (_inputStream.hasBytesAvailable) {
+ NSInteger bytes_read = [_inputStream read:buffer maxLength:bufferSize];
+
+ if (bytes_read > 0) {
+ [_readBuffer appendBytes:buffer length:bytes_read];
+ } else if (bytes_read < 0) {
+ [self _failWithError:_inputStream.streamError];
+ }
+
+ if (bytes_read != bufferSize) {
+ break;
+ }
+ };
+ [self _pumpScanner];
+ break;
+ }
+
+ case NSStreamEventHasSpaceAvailable: {
+ SRFastLog(@"NSStreamEventHasSpaceAvailable %@", aStream);
+ [self _pumpWriting];
+ break;
+ }
+
+ default:
+ SRFastLog(@"(default) %@", aStream);
+ break;
+ }
+}
+
+@end
+
+
+@implementation FSRIOConsumer
+
+@synthesize bytesNeeded = _bytesNeeded;
+@synthesize consumer = _scanner;
+@synthesize handler = _handler;
+@synthesize readToCurrentFrame = _readToCurrentFrame;
+@synthesize unmaskBytes = _unmaskBytes;
+
+- (void)setupWithScanner:(stream_scanner)scanner handler:(data_callback)handler bytesNeeded:(size_t)bytesNeeded readToCurrentFrame:(BOOL)readToCurrentFrame unmaskBytes:(BOOL)unmaskBytes;
+{
+ _scanner = [scanner copy];
+ _handler = [handler copy];
+ _bytesNeeded = bytesNeeded;
+ _readToCurrentFrame = readToCurrentFrame;
+ _unmaskBytes = unmaskBytes;
+ assert(_scanner || _bytesNeeded);
+}
+
+@end
+
+@implementation FSRIOConsumerPool {
+ NSUInteger _poolSize;
+ NSMutableArray *_bufferedConsumers;
+}
+
+- (id)initWithBufferCapacity:(NSUInteger)poolSize;
+{
+ self = [super init];
+ if (self) {
+ _poolSize = poolSize;
+ _bufferedConsumers = [[NSMutableArray alloc] initWithCapacity:poolSize];
+ }
+ return self;
+}
+
+- (id)init
+{
+ return [self initWithBufferCapacity:8];
+}
+
+- (FSRIOConsumer *)consumerWithScanner:(stream_scanner)scanner handler:(data_callback)handler bytesNeeded:(size_t)bytesNeeded readToCurrentFrame:(BOOL)readToCurrentFrame unmaskBytes:(BOOL)unmaskBytes;
+{
+ FSRIOConsumer *consumer = nil;
+ if (_bufferedConsumers.count) {
+ consumer = [_bufferedConsumers lastObject];
+ [_bufferedConsumers removeLastObject];
+ } else {
+ consumer = [[FSRIOConsumer alloc] init];
+ }
+
+ [consumer setupWithScanner:scanner handler:handler bytesNeeded:bytesNeeded readToCurrentFrame:readToCurrentFrame unmaskBytes:unmaskBytes];
+
+ return consumer;
+}
+
+- (void)returnConsumer:(FSRIOConsumer *)consumer;
+{
+ if (_bufferedConsumers.count < _poolSize) {
+ [_bufferedConsumers addObject:consumer];
+ }
+}
+
+@end
+
+@implementation NSURLRequest (FCertificateAdditions)
+
+- (NSArray *)FSR_SSLPinnedCertificates;
+{
+ return [NSURLProtocol propertyForKey:@"FSR_SSLPinnedCertificates" inRequest:self];
+}
+
+@end
+
+@implementation NSMutableURLRequest (FCertificateAdditions)
+
+- (NSArray *)FSR_SSLPinnedCertificates;
+{
+ return [NSURLProtocol propertyForKey:@"FSR_SSLPinnedCertificates" inRequest:self];
+}
+
+- (void)setFSR_SSLPinnedCertificates:(NSArray *)FSR_SSLPinnedCertificates;
+{
+ [NSURLProtocol setProperty:FSR_SSLPinnedCertificates forKey:@"FSR_SSLPinnedCertificates" inRequest:self];
+}
+
+@end
+
+@implementation NSURL (FSRWebSocket)
+
+- (NSString *)SR_origin;
+{
+ NSString *scheme = [self.scheme lowercaseString];
+
+ if ([scheme isEqualToString:@"wss"]) {
+ scheme = @"https";
+ } else if ([scheme isEqualToString:@"ws"]) {
+ scheme = @"http";
+ }
+
+ if (self.port) {
+ return [NSString stringWithFormat:@"%@://%@:%@/", scheme, self.host, self.port];
+ } else {
+ return [NSString stringWithFormat:@"%@://%@/", scheme, self.host];
+ }
+}
+
+@end
+
+// #define SR_ENABLE_LOG
+
+static inline void SRFastLog(NSString *format, ...) {
+#ifdef SR_ENABLE_LOG
+ __block va_list arg_list;
+ va_start (arg_list, format);
+
+ NSString *formattedString = [[NSString alloc] initWithFormat:format arguments:arg_list];
+
+ va_end(arg_list);
+
+ NSLog(@"[SR] %@", formattedString);
+#endif
+}
+
+
+#ifdef HAS_ICU
+
+static inline int32_t validate_dispatch_data_partial_string(NSData *data) {
+
+ const void * contents = [data bytes];
+ long size = [data length];
+
+ const uint8_t *str = (const uint8_t *)contents;
+
+
+ UChar32 codepoint = 1;
+ int32_t offset = 0;
+ int32_t lastOffset = 0;
+ while(offset < size && codepoint > 0) {
+ lastOffset = offset;
+ U8_NEXT(str, offset, size, codepoint);
+ }
+
+ if (codepoint == -1) {
+ // Check to see if the last byte is valid or whether it was just continuing
+ if (!U8_IS_LEAD(str[lastOffset]) || U8_COUNT_TRAIL_BYTES(str[lastOffset]) + lastOffset < (int32_t)size) {
+
+ size = -1;
+ } else {
+ uint8_t leadByte = str[lastOffset];
+ U8_MASK_LEAD_BYTE(leadByte, U8_COUNT_TRAIL_BYTES(leadByte));
+
+ for (int i = lastOffset + 1; i < offset; i++) {
+
+ if (U8_IS_SINGLE(str[i]) || U8_IS_LEAD(str[i]) || !U8_IS_TRAIL(str[i])) {
+ size = -1;
+ }
+ }
+
+ if (size != -1) {
+ size = lastOffset;
+ }
+ }
+ }
+
+ if (size != -1 && ![[NSString alloc] initWithBytesNoCopy:(char *)[data bytes] length:size encoding:NSUTF8StringEncoding freeWhenDone:NO]) {
+ size = -1;
+ }
+
+ return (int32_t)size;
+}
+
+#else
+
+// This is a hack, and probably not optimal
+static inline int32_t validate_dispatch_data_partial_string(NSData *data) {
+ static const int maxCodepointSize = 3;
+
+ for (int i = 0; i < maxCodepointSize; i++) {
+ NSString *str = [[NSString alloc] initWithBytesNoCopy:(char *)data.bytes length:data.length - i encoding:NSUTF8StringEncoding freeWhenDone:NO];
+ if (str) {
+ return (int)(data.length - i);
+ }
+ }
+
+ return -1;
+}
+
+#endif
+
+static _FSRRunLoopThread *networkThread = nil;
+static NSRunLoop *networkRunLoop = nil;
+
+@implementation NSRunLoop (FSRWebSocket)
+
++ (NSRunLoop *)FSR_networkRunLoop {
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ networkThread = [[_FSRRunLoopThread alloc] init];
+ networkThread.name = @"com.squareup.SocketRocket.NetworkThread";
+ [networkThread start];
+ networkRunLoop = networkThread.runLoop;
+ });
+
+ return networkRunLoop;
+}
+
+@end
+
+
+@implementation _FSRRunLoopThread {
+ dispatch_group_t _waitGroup;
+}
+
+@synthesize runLoop = _runLoop;
+
+- (void)dealloc
+{
+ sr_dispatch_release(_waitGroup);
+}
+
+- (id)init
+{
+ self = [super init];
+ if (self) {
+ _waitGroup = dispatch_group_create();
+ dispatch_group_enter(_waitGroup);
+ }
+ return self;
+}
+
+
+/**
+ * This is the main method of the thread on which the socket events are scheduled in a run loop.
+ */
+- (void)main;
+{
+ @autoreleasepool {
+ _runLoop = [NSRunLoop currentRunLoop];
+ dispatch_group_leave(_waitGroup);
+
+ // Add an empty run loop source to prevent runloop from spinning.
+ CFRunLoopSourceContext sourceCtx = {
+ .version = 0,
+ .info = NULL,
+ .retain = NULL,
+ .release = NULL,
+ .copyDescription = NULL,
+ .equal = NULL,
+ .hash = NULL,
+ .schedule = NULL,
+ .cancel = NULL,
+ .perform = NULL
+ };
+ CFRunLoopSourceRef source = CFRunLoopSourceCreate(NULL, 0, &sourceCtx);
+ CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
+ CFRelease(source);
+
+ while ([_runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]) {
+
+ }
+ assert(NO);
+ }
+}
+
+- (NSRunLoop *)runLoop;
+{
+ dispatch_group_wait(_waitGroup, DISPATCH_TIME_FOREVER);
+ return _runLoop;
+}
+
+@end
diff --git a/Firebase/Database/third_party/SocketRocket/NSData+SRB64Additions.h b/Firebase/Database/third_party/SocketRocket/NSData+SRB64Additions.h
new file mode 100644
index 0000000..bac393b
--- /dev/null
+++ b/Firebase/Database/third_party/SocketRocket/NSData+SRB64Additions.h
@@ -0,0 +1,23 @@
+//
+// Copyright 2012 Square Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+#import <Foundation/Foundation.h>
+
+@interface FSRUtilities : NSObject
+
++ (NSString *)base64EncodedStringFromData:(NSData *)data;
+
+@end
diff --git a/Firebase/Database/third_party/SocketRocket/NSData+SRB64Additions.m b/Firebase/Database/third_party/SocketRocket/NSData+SRB64Additions.m
new file mode 100644
index 0000000..2be1d84
--- /dev/null
+++ b/Firebase/Database/third_party/SocketRocket/NSData+SRB64Additions.m
@@ -0,0 +1,37 @@
+//
+// Copyright 2012 Square Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+#import "NSData+SRB64Additions.h"
+#import "fbase64.h"
+
+@implementation FSRUtilities
+
++ (NSString *)base64EncodedStringFromData:(NSData *)data {
+ size_t buffer_size = ((data.length * 3 + 2) / 2);
+
+ char *buffer = (char *)malloc(buffer_size);
+
+ int len = f_b64_ntop(data.bytes, data.length, buffer, buffer_size);
+
+ if (len == -1) {
+ free(buffer);
+ return nil;
+ } else{
+ return [[NSString alloc] initWithBytesNoCopy:buffer length:len encoding:NSUTF8StringEncoding freeWhenDone:YES];
+ }
+}
+
+@end
diff --git a/Firebase/Database/third_party/SocketRocket/aa2297808c225710e267afece4439c256f6efdb3 b/Firebase/Database/third_party/SocketRocket/aa2297808c225710e267afece4439c256f6efdb3
new file mode 100644
index 0000000..152c47c
--- /dev/null
+++ b/Firebase/Database/third_party/SocketRocket/aa2297808c225710e267afece4439c256f6efdb3
@@ -0,0 +1,3 @@
+Fri Aug 3 15:45:39 PDT 2012
+Github commit: aa2297808c225710e267afece4439c256f6efdb3
+
diff --git a/Firebase/Database/third_party/SocketRocket/fbase64.c b/Firebase/Database/third_party/SocketRocket/fbase64.c
new file mode 100644
index 0000000..1750673
--- /dev/null
+++ b/Firebase/Database/third_party/SocketRocket/fbase64.c
@@ -0,0 +1,318 @@
+/* $OpenBSD: base64.c,v 1.5 2006/10/21 09:55:03 otto Exp $ */
+
+/*
+ * Copyright (c) 1996 by Internet Software Consortium.
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SOFTWARE CONSORTIUM DISCLAIMS
+ * ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL INTERNET SOFTWARE
+ * CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL
+ * DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR
+ * PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS
+ * ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS
+ * SOFTWARE.
+ */
+
+/*
+ * Portions Copyright (c) 1995 by International Business Machines, Inc.
+ *
+ * International Business Machines, Inc. (hereinafter called IBM) grants
+ * permission under its copyrights to use, copy, modify, and distribute this
+ * Software with or without fee, provided that the above copyright notice and
+ * all paragraphs of this notice appear in all copies, and that the name of IBM
+ * not be used in connection with the marketing of any product incorporating
+ * the Software or modifications thereof, without specific, written prior
+ * permission.
+ *
+ * To the extent it has a right to do so, IBM grants an immunity from suit
+ * under its patents, if any, for the use, sale or manufacture of products to
+ * the extent that such products are used for performing Domain Name System
+ * dynamic updates in TCP/IP networks by means of the Software. No immunity is
+ * granted for any product per se or for any other function of any product.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", AND IBM DISCLAIMS ALL WARRANTIES,
+ * INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+ * PARTICULAR PURPOSE. IN NO EVENT SHALL IBM BE LIABLE FOR ANY SPECIAL,
+ * DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER ARISING
+ * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE, EVEN
+ * IF IBM IS APPRISED OF THE POSSIBILITY OF SUCH DAMAGES.
+ */
+
+/* OPENBSD ORIGINAL: lib/libc/net/base64.c */
+
+
+//
+// Distributed with modifications by Firebase ( https://www.firebase.com )
+//
+
+#if (!defined(HAVE_B64_NTOP) && !defined(HAVE___B64_NTOP)) || (!defined(HAVE_B64_PTON) && !defined(HAVE___B64_PTON))
+
+#include <sys/types.h>
+#include <sys/param.h>
+#include <sys/socket.h>
+#include <netinet/in.h>
+#include <arpa/inet.h>
+
+#include <ctype.h>
+#include <stdio.h>
+
+#include <stdlib.h>
+#include <string.h>
+
+#include "fbase64.h"
+
+static const char Base64[] =
+"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+static const char Pad64 = '=';
+
+/* (From RFC1521 and draft-ietf-dnssec-secext-03.txt)
+ The following encoding technique is taken from RFC 1521 by Borenstein
+ and Freed. It is reproduced here in a slightly edited form for
+ convenience.
+
+ A 65-character subset of US-ASCII is used, enabling 6 bits to be
+ represented per printable character. (The extra 65th character, "=",
+ is used to signify a special processing function.)
+
+ The encoding process represents 24-bit groups of input bits as output
+ strings of 4 encoded characters. Proceeding from left to right, a
+ 24-bit input group is formed by concatenating 3 8-bit input groups.
+ These 24 bits are then treated as 4 concatenated 6-bit groups, each
+ of which is translated into a single digit in the base64 alphabet.
+
+ Each 6-bit group is used as an index into an array of 64 printable
+ characters. The character referenced by the index is placed in the
+ output string.
+
+ Table 1: The Base64 Alphabet
+
+ Value Encoding Value Encoding Value Encoding Value Encoding
+ 0 A 17 R 34 i 51 z
+ 1 B 18 S 35 j 52 0
+ 2 C 19 T 36 k 53 1
+ 3 D 20 U 37 l 54 2
+ 4 E 21 V 38 m 55 3
+ 5 F 22 W 39 n 56 4
+ 6 G 23 X 40 o 57 5
+ 7 H 24 Y 41 p 58 6
+ 8 I 25 Z 42 q 59 7
+ 9 J 26 a 43 r 60 8
+ 10 K 27 b 44 s 61 9
+ 11 L 28 c 45 t 62 +
+ 12 M 29 d 46 u 63 /
+ 13 N 30 e 47 v
+ 14 O 31 f 48 w (pad) =
+ 15 P 32 g 49 x
+ 16 Q 33 h 50 y
+
+ Special processing is performed if fewer than 24 bits are available
+ at the end of the data being encoded. A full encoding quantum is
+ always completed at the end of a quantity. When fewer than 24 input
+ bits are available in an input group, zero bits are added (on the
+ right) to form an integral number of 6-bit groups. Padding at the
+ end of the data is performed using the '=' character.
+
+ Since all base64 input is an integral number of octets, only the
+ -------------------------------------------------
+ following cases can arise:
+
+ (1) the final quantum of encoding input is an integral
+ multiple of 24 bits; here, the final unit of encoded
+ output will be an integral multiple of 4 characters
+ with no "=" padding,
+ (2) the final quantum of encoding input is exactly 8 bits;
+ here, the final unit of encoded output will be two
+ characters followed by two "=" padding characters, or
+ (3) the final quantum of encoding input is exactly 16 bits;
+ here, the final unit of encoded output will be three
+ characters followed by one "=" padding character.
+ */
+
+#if !defined(HAVE_B64_NTOP) && !defined(HAVE___B64_NTOP)
+int
+f_b64_ntop(u_char const *src, size_t srclength, char *target, size_t targsize)
+{
+ size_t datalength = 0;
+ u_char input[3];
+ u_char output[4];
+ u_int i;
+
+ while (2 < srclength) {
+ input[0] = *src++;
+ input[1] = *src++;
+ input[2] = *src++;
+ srclength -= 3;
+
+ output[0] = input[0] >> 2;
+ output[1] = ((input[0] & 0x03) << 4) + (input[1] >> 4);
+ output[2] = ((input[1] & 0x0f) << 2) + (input[2] >> 6);
+ output[3] = input[2] & 0x3f;
+
+ if (datalength + 4 > targsize)
+ return (-1);
+ target[datalength++] = Base64[output[0]];
+ target[datalength++] = Base64[output[1]];
+ target[datalength++] = Base64[output[2]];
+ target[datalength++] = Base64[output[3]];
+ }
+
+ /* Now we worry about padding. */
+ if (0 != srclength) {
+ /* Get what's left. */
+ input[0] = input[1] = input[2] = '\0';
+ for (i = 0; i < srclength; i++)
+ input[i] = *src++;
+
+ output[0] = input[0] >> 2;
+ output[1] = ((input[0] & 0x03) << 4) + (input[1] >> 4);
+ output[2] = ((input[1] & 0x0f) << 2) + (input[2] >> 6);
+
+ if (datalength + 4 > targsize)
+ return (-1);
+ target[datalength++] = Base64[output[0]];
+ target[datalength++] = Base64[output[1]];
+ if (srclength == 1)
+ target[datalength++] = Pad64;
+ else
+ target[datalength++] = Base64[output[2]];
+ target[datalength++] = Pad64;
+ }
+ if (datalength >= targsize)
+ return (-1);
+ target[datalength] = '\0'; /* Returned value doesn't count \0. */
+ return (int)(datalength);
+}
+#endif /* !defined(HAVE_B64_NTOP) && !defined(HAVE___B64_NTOP) */
+
+#if !defined(HAVE_B64_PTON) && !defined(HAVE___B64_PTON)
+
+/* skips all whitespace anywhere.
+ converts characters, four at a time, starting at (or after)
+ src from base - 64 numbers into three 8 bit bytes in the target area.
+ it returns the number of data bytes stored at the target, or -1 on error.
+ */
+
+int
+f_b64_pton(char const *src, u_char *target, size_t targsize)
+{
+ u_int tarindex, state;
+ int ch;
+ char *pos;
+
+ state = 0;
+ tarindex = 0;
+
+ while ((ch = *src++) != '\0') {
+ if (isspace(ch)) /* Skip whitespace anywhere. */
+ continue;
+
+ if (ch == Pad64)
+ break;
+
+ pos = strchr(Base64, ch);
+ if (pos == 0) /* A non-base64 character. */
+ return (-1);
+
+ switch (state) {
+ case 0:
+ if (target) {
+ if (tarindex >= targsize)
+ return (-1);
+ target[tarindex] = (pos - Base64) << 2;
+ }
+ state = 1;
+ break;
+ case 1:
+ if (target) {
+ if (tarindex + 1 >= targsize)
+ return (-1);
+ target[tarindex] |= (pos - Base64) >> 4;
+ target[tarindex+1] = ((pos - Base64) & 0x0f)
+ << 4 ;
+ }
+ tarindex++;
+ state = 2;
+ break;
+ case 2:
+ if (target) {
+ if (tarindex + 1 >= targsize)
+ return (-1);
+ target[tarindex] |= (pos - Base64) >> 2;
+ target[tarindex+1] = ((pos - Base64) & 0x03)
+ << 6;
+ }
+ tarindex++;
+ state = 3;
+ break;
+ case 3:
+ if (target) {
+ if (tarindex >= targsize)
+ return (-1);
+ target[tarindex] |= (pos - Base64);
+ }
+ tarindex++;
+ state = 0;
+ break;
+ }
+ }
+
+ /*
+ * We are done decoding Base-64 chars. Let's see if we ended
+ * on a byte boundary, and/or with erroneous trailing characters.
+ */
+
+ if (ch == Pad64) { /* We got a pad char. */
+ ch = *src++; /* Skip it, get next. */
+ switch (state) {
+ case 0: /* Invalid = in first position */
+ case 1: /* Invalid = in second position */
+ return (-1);
+
+ case 2: /* Valid, means one byte of info */
+ /* Skip any number of spaces. */
+ for (; ch != '\0'; ch = *src++)
+ if (!isspace(ch))
+ break;
+ /* Make sure there is another trailing = sign. */
+ if (ch != Pad64)
+ return (-1);
+ ch = *src++; /* Skip the = */
+ /* Fall through to "single trailing =" case. */
+ /* FALLTHROUGH */
+
+ case 3: /* Valid, means two bytes of info */
+ /*
+ * We know this char is an =. Is there anything but
+ * whitespace after it?
+ */
+ for (; ch != '\0'; ch = *src++)
+ if (!isspace(ch))
+ return (-1);
+
+ /*
+ * Now make sure for cases 2 and 3 that the "extra"
+ * bits that slopped past the last full byte were
+ * zeros. If we don't check them, they become a
+ * subliminal channel.
+ */
+ if (target && target[tarindex] != 0)
+ return (-1);
+ }
+ } else {
+ /*
+ * We ended by seeing the end of the string. Make sure we
+ * have no partial bytes lying around.
+ */
+ if (state != 0)
+ return (-1);
+ }
+
+ return (tarindex);
+}
+
+#endif /* !defined(HAVE_B64_PTON) && !defined(HAVE___B64_PTON) */
+#endif \ No newline at end of file
diff --git a/Firebase/Database/third_party/SocketRocket/fbase64.h b/Firebase/Database/third_party/SocketRocket/fbase64.h
new file mode 100644
index 0000000..a9c55c9
--- /dev/null
+++ b/Firebase/Database/third_party/SocketRocket/fbase64.h
@@ -0,0 +1,33 @@
+// Copyright 2012 Square Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+#ifndef FSocketRocket_base64_h
+#define FSocketRocket_base64_h
+
+#include <sys/types.h>
+
+extern int
+f_b64_ntop(u_char const *src,
+ size_t srclength,
+ char *target,
+ size_t targsize);
+
+extern int
+f_b64_pton(char const *src,
+ u_char *target,
+ size_t targsize);
+
+
+#endif
diff --git a/Firebase/Database/third_party/Wrap-leveldb/APLevelDB.h b/Firebase/Database/third_party/Wrap-leveldb/APLevelDB.h
new file mode 100644
index 0000000..c0baa22
--- /dev/null
+++ b/Firebase/Database/third_party/Wrap-leveldb/APLevelDB.h
@@ -0,0 +1,105 @@
+//
+// APLevelDB.h
+//
+// Created by Adam Preble on 1/23/12.
+// Copyright (c) 2012 Adam Preble. All rights reserved.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+#import <Foundation/Foundation.h>
+
+extern NSString * const APLevelDBErrorDomain;
+
+@class APLevelDBIterator;
+@protocol APLevelDBWriteBatch;
+
+@interface APLevelDB : NSObject
+
+@property (nonatomic, readonly, strong) NSString *path;
+
++ (APLevelDB *)levelDBWithPath:(NSString *)path error:(NSError *__autoreleasing*)errorOut;
+- (void)close;
+
+- (BOOL)setData:(NSData *)data forKey:(NSString *)key;
+- (BOOL)setString:(NSString *)str forKey:(NSString *)key;
+
+- (NSData *)dataForKey:(NSString *)key;
+- (NSString *)stringForKey:(NSString *)key;
+
+- (BOOL)removeKey:(NSString *)key;
+
+- (NSArray *)allKeys;
+
+- (void)enumerateKeys:(void (^)(NSString *key, BOOL *stop))block;
+- (void)enumerateKeysWithPrefix:(NSString *)prefix usingBlock:(void (^)(NSString *key, BOOL *stop))block;
+
+- (void)enumerateKeysAndValuesAsStrings:(void (^)(NSString *key, NSString *value, BOOL *stop))block;
+- (void)enumerateKeysWithPrefix:(NSString *)prefix asStrings:(void (^)(NSString *key, NSString *value, BOOL *stop))block;
+
+- (void)enumerateKeysAndValuesAsData:(void (^)(NSString *key, NSData *value, BOOL *stop))block;
+- (void)enumerateKeysWithPrefix:(NSString *)prefix asData:(void (^)(NSString *key, NSData *value, BOOL *stop))block;
+
+- (NSUInteger)approximateSizeFrom:(NSString *)from to:(NSString *)to;
+- (NSUInteger)exactSizeFrom:(NSString *)from to:(NSString *)to;
+
+// Objective-C Subscripting Support:
+// The database object supports subscripting for string-string and string-data key-value access and assignment.
+// Examples:
+// db[@"key"] = @"value";
+// db[@"key"] = [NSData data];
+// NSString *s = db[@"key"];
+// An NSInvalidArgumentException is raised if the key is not an NSString, or if the assigned object is not an
+// instance of NSString or NSData.
+- (id)objectForKeyedSubscript:(id)key;
+- (void)setObject:(id)object forKeyedSubscript:(id<NSCopying>)key;
+
+// Batch write/atomic update support:
+- (id<APLevelDBWriteBatch>)beginWriteBatch;
+
+@end
+
+
+@interface APLevelDBIterator : NSObject
+
++ (id)iteratorWithLevelDB:(APLevelDB *)db;
+
+// Designated initializer:
+- (id)initWithLevelDB:(APLevelDB *)db;
+
+- (BOOL)seekToKey:(NSString *)key;
+- (NSString *)nextKey;
+- (NSString *)key;
+- (NSString *)valueAsString;
+- (NSData *)valueAsData;
+
+@end
+
+
+@protocol APLevelDBWriteBatch <NSObject>
+
+- (void)setData:(NSData *)data forKey:(NSString *)key;
+- (void)setString:(NSString *)str forKey:(NSString *)key;
+
+- (void)removeKey:(NSString *)key;
+
+// Remove all of the buffered sets and removes:
+- (void)clear;
+- (BOOL)commit;
+
+@end
diff --git a/Firebase/Database/third_party/Wrap-leveldb/APLevelDB.mm b/Firebase/Database/third_party/Wrap-leveldb/APLevelDB.mm
new file mode 100644
index 0000000..cdecce6
--- /dev/null
+++ b/Firebase/Database/third_party/Wrap-leveldb/APLevelDB.mm
@@ -0,0 +1,500 @@
+//
+// APLevelDB.m
+//
+// Created by Adam Preble on 1/23/12.
+// Copyright (c) 2012 Adam Preble. All rights reserved.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+//
+// Portions of APLevelDB are based on LevelDB-ObjC:
+// https://github.com/hoisie/LevelDB-ObjC
+// Specifically the SliceFromString/StringFromSlice macros, and the structure of
+// the enumeration methods. License for those potions follows:
+//
+// Copyright (c) 2011 Pave Labs
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+//
+
+#import "APLevelDB.h"
+
+#import "leveldb/db.h"
+#import "leveldb/options.h"
+#import "leveldb/write_batch.h"
+
+NSString * const APLevelDBErrorDomain = @"APLevelDBErrorDomain";
+
+#define SliceFromString(_string_) (leveldb::Slice((char *)[_string_ UTF8String], [_string_ lengthOfBytesUsingEncoding:NSUTF8StringEncoding]))
+#define StringFromSlice(_slice_) ([[NSString alloc] initWithBytes:_slice_.data() length:_slice_.size() encoding:NSUTF8StringEncoding])
+
+
+@interface APLevelDBWriteBatch : NSObject <APLevelDBWriteBatch> {
+ @package
+ leveldb::WriteBatch _batch;
+}
+
+@property (nonatomic, strong) APLevelDB *levelDB;
+
+- (id)initWithLevelDB:(APLevelDB *)levelDB;
+@end
+
+
+#pragma mark - APLevelDB
+
+@interface APLevelDB () {
+ leveldb::DB *_db;
+ leveldb::ReadOptions _readOptions;
+ leveldb::WriteOptions _writeOptions;
+}
+- (id)initWithPath:(NSString *)path error:(NSError **)errorOut;
++ (leveldb::Options)defaultCreateOptions;
+@property (nonatomic, readonly) leveldb::DB *db;
+@end
+
+
+@implementation APLevelDB
+
+@synthesize path = _path;
+@synthesize db = _db;
+
++ (APLevelDB *)levelDBWithPath:(NSString *)path error:(NSError *__autoreleasing *)errorOut
+{
+ return [[APLevelDB alloc] initWithPath:path error:errorOut];
+}
+
+- (id)initWithPath:(NSString *)path error:(NSError *__autoreleasing *)errorOut
+{
+ if ((self = [super init]))
+ {
+ _path = path;
+
+ leveldb::Options options = [[self class] defaultCreateOptions];
+
+ leveldb::Status status = leveldb::DB::Open(options, [_path UTF8String], &_db);
+
+ if (!status.ok())
+ {
+ if (errorOut)
+ {
+ NSString *statusString = [[NSString alloc] initWithCString:status.ToString().c_str() encoding:NSUTF8StringEncoding];
+ *errorOut = [NSError errorWithDomain:APLevelDBErrorDomain
+ code:0
+ userInfo:[NSDictionary dictionaryWithObjectsAndKeys:statusString, NSLocalizedDescriptionKey, nil]];
+ }
+ return nil;
+ }
+
+ _writeOptions.sync = false;
+ }
+ return self;
+}
+
+- (void)close {
+ if (_db != NULL) {
+ delete _db;
+ _db = NULL;
+ }
+}
+
+- (void)dealloc
+{
+ if (_db != NULL) {
+ delete _db;
+ _db = NULL;
+ }
+}
+
++ (leveldb::Options)defaultCreateOptions
+{
+ leveldb::Options options;
+ options.create_if_missing = true;
+ return options;
+}
+
+- (BOOL)setData:(NSData *)data forKey:(NSString *)key
+{
+ leveldb::Slice keySlice = SliceFromString(key);
+ leveldb::Slice valueSlice = leveldb::Slice((const char *)[data bytes], (size_t)[data length]);
+ leveldb::Status status = _db->Put(_writeOptions, keySlice, valueSlice);
+ return (status.ok() == true);
+}
+
+- (BOOL)setString:(NSString *)str forKey:(NSString *)key
+{
+ // This could have been based on
+ leveldb::Slice keySlice = SliceFromString(key);
+ leveldb::Slice valueSlice = SliceFromString(str);
+ leveldb::Status status = _db->Put(_writeOptions, keySlice, valueSlice);
+ return (status.ok() == true);
+}
+
+- (NSData *)dataForKey:(NSString *)key
+{
+ leveldb::Slice keySlice = SliceFromString(key);
+ std::string valueCPPString;
+ leveldb::Status status = _db->Get(_readOptions, keySlice, &valueCPPString);
+
+ if (!status.ok())
+ return nil;
+ else
+ return [NSData dataWithBytes:valueCPPString.data() length:valueCPPString.size()];
+}
+
+- (NSString *)stringForKey:(NSString *)key
+{
+ leveldb::Slice keySlice = SliceFromString(key);
+ std::string valueCPPString;
+ leveldb::Status status = _db->Get(_readOptions, keySlice, &valueCPPString);
+
+ // We assume (dangerously?) UTF-8 string encoding:
+ if (!status.ok())
+ return nil;
+ else
+ return [[NSString alloc] initWithBytes:valueCPPString.data() length:valueCPPString.size() encoding:NSUTF8StringEncoding];
+}
+
+- (BOOL)removeKey:(NSString *)key
+{
+ leveldb::Slice keySlice = SliceFromString(key);
+ leveldb::Status status = _db->Delete(_writeOptions, keySlice);
+ return (status.ok() == true);
+}
+
+- (NSArray *)allKeys
+{
+ NSMutableArray *keys = [NSMutableArray array];
+ [self enumerateKeys:^(NSString *key, BOOL *stop) {
+ [keys addObject:key];
+ }];
+ return keys;
+}
+
+- (void)enumerateKeysAndValuesAsStrings:(void (^)(NSString *key, NSString *value, BOOL *stop))block
+{
+ [self enumerateKeysWithPrefix:@"" asStrings:block];
+}
+
+- (void)enumerateKeysWithPrefix:(NSString *)prefixString asStrings:(void (^)(NSString *, NSString *, BOOL *))block
+{
+ @autoreleasepool {
+ BOOL stop = NO;
+ leveldb::Iterator* iter = _db->NewIterator(leveldb::ReadOptions());
+ leveldb::Slice prefix = SliceFromString(prefixString);
+ for (iter->Seek(prefix); iter->Valid(); iter->Next()) {
+ leveldb::Slice key = iter->key(), value = iter->value();
+ if (key.starts_with(prefix)) {
+ NSString *k = StringFromSlice(key);
+ NSString *v = [[NSString alloc] initWithBytes:value.data() length:value.size() encoding:NSUTF8StringEncoding];
+ block(k, v, &stop);
+ if (stop)
+ break;
+ } else {
+ break;
+ }
+ }
+
+ delete iter;
+ }
+}
+
+- (void)enumerateKeys:(void (^)(NSString *key, BOOL *stop))block
+{
+ [self enumerateKeysWithPrefix:@"" usingBlock:block];
+}
+
+- (void)enumerateKeysWithPrefix:(NSString *)prefixString usingBlock:(void (^)(NSString *key, BOOL *stop))block;
+{
+ @autoreleasepool {
+ BOOL stop = NO;
+ leveldb::Slice prefix = SliceFromString(prefixString);
+ leveldb::Iterator* iter = _db->NewIterator(leveldb::ReadOptions());
+ for (iter->Seek(prefix); iter->Valid(); iter->Next()) {
+ leveldb::Slice key = iter->key();
+ if (key.starts_with(prefix)) {
+ NSString *k = StringFromSlice(key);
+ block(k, &stop);
+ if (stop)
+ break;
+ } else {
+ break;
+ }
+ }
+
+ delete iter;
+ }
+}
+
+- (void)enumerateKeysAndValuesAsData:(void (^)(NSString *key, NSData *data, BOOL *stop))block
+{
+ [self enumerateKeysWithPrefix:@"" asData:block];
+}
+
+- (void)enumerateKeysWithPrefix:(NSString *)prefixString asData:(void (^)(NSString *, NSData *, BOOL *))block
+{
+ @autoreleasepool {
+ BOOL stop = NO;
+ leveldb::Iterator* iter = _db->NewIterator(leveldb::ReadOptions());
+ leveldb::Slice prefix = SliceFromString(prefixString);
+ for (iter->Seek(prefix); iter->Valid(); iter->Next()) {
+ leveldb::Slice key = iter->key(), value = iter->value();
+ if (key.starts_with(prefix)) {
+ NSString *k = StringFromSlice(key);
+ NSData *data = [NSData dataWithBytes:value.data() length:value.size()];
+ block(k, data, &stop);
+ if (stop)
+ break;
+ } else {
+ break;
+ }
+ }
+
+ delete iter;
+ }
+}
+
+- (NSUInteger)exactSizeFrom:(NSString *)from to:(NSString *)to {
+ NSUInteger size = 0;
+ leveldb::Iterator* iter = _db->NewIterator(leveldb::ReadOptions());
+ leveldb::Slice fromSlice = SliceFromString(from);
+ leveldb::Slice toSlice = SliceFromString(to);
+ iter->Seek(fromSlice);
+ while (iter->Valid() && iter->key().compare(toSlice) <= 0) {
+ size += iter->value().size();
+ iter->Next();
+ }
+ delete iter;
+ return size;
+}
+
+
+- (NSUInteger)approximateSizeFrom:(NSString *)from to:(NSString *)to {
+ leveldb::Range ranges[1];
+ leveldb::Slice fromSlice = SliceFromString(from);
+ leveldb::Slice toSlice = SliceFromString(to);
+ ranges[0] = leveldb::Range(fromSlice, toSlice);
+ uint64_t sizes[1];
+ _db->GetApproximateSizes(ranges, 1, sizes);
+ return (NSUInteger)sizes[0];
+}
+
+#pragma mark - Subscripting Support
+
+- (id)objectForKeyedSubscript:(id)key
+{
+ if (![key respondsToSelector: @selector(componentsSeparatedByString:)])
+ {
+ [NSException raise:NSInvalidArgumentException format:@"key must be an NSString"];
+ }
+ return [self stringForKey:key];
+}
+- (void)setObject:(id)thing forKeyedSubscript:(id<NSCopying>)key
+{
+ id idKey = (id) key;
+ if (![idKey respondsToSelector: @selector(componentsSeparatedByString:)])
+ {
+ [NSException raise:NSInvalidArgumentException format:@"key must be NSString or NSData"];
+ }
+
+ if ([thing respondsToSelector:@selector(componentsSeparatedByString:)])
+ [self setString:thing forKey:(NSString *)key];
+ else if ([thing respondsToSelector:@selector(subdataWithRange:)])
+ [self setData:thing forKey:(NSString *)key];
+ else
+ [NSException raise:NSInvalidArgumentException format:@"object must be NSString or NSData"];
+}
+
+#pragma mark - Atomic Updates
+
+- (id<APLevelDBWriteBatch>)beginWriteBatch
+{
+ APLevelDBWriteBatch *batch = [[APLevelDBWriteBatch alloc] initWithLevelDB:self];
+ return batch;
+}
+
+- (BOOL)commitWriteBatch:(id<APLevelDBWriteBatch>)theBatch
+{
+ if (!theBatch)
+ return NO;
+
+ APLevelDBWriteBatch *batch = theBatch;
+
+ leveldb::Status status;
+ status = _db->Write(_writeOptions, &batch->_batch);
+ return (status.ok() == true);
+}
+
+@end
+
+
+#pragma mark - APLevelDBIterator
+
+@interface APLevelDBIterator () {
+ leveldb::Iterator *_iter;
+}
+
+@property (nonatomic, strong) APLevelDB *levelDB;
+@end
+
+
+
+@implementation APLevelDBIterator
+
++ (id)iteratorWithLevelDB:(APLevelDB *)db
+{
+ APLevelDBIterator *iter = [[[self class] alloc] initWithLevelDB:db];
+ return iter;
+}
+
+- (id)initWithLevelDB:(APLevelDB *)db
+{
+ if ((self = [super init]))
+ {
+ // Hold on to the database so it doesn't get deallocated before the iterator is deallocated
+ self->_levelDB = db;
+ _iter = db.db->NewIterator(leveldb::ReadOptions());
+ _iter->SeekToFirst();
+ if (!_iter->Valid())
+ return nil;
+ }
+ return self;
+}
+
+- (id)init
+{
+ [NSException raise:@"BadInitializer" format:@"Use the designated initializer, -initWithLevelDB:, instead."];
+ return nil;
+}
+
+- (void)dealloc
+{
+ self->_levelDB = nil;
+ delete _iter;
+ _iter = NULL;
+}
+
+- (BOOL)seekToKey:(NSString *)key
+{
+ leveldb::Slice target = SliceFromString(key);
+ _iter->Seek(target);
+ return _iter->Valid() == true;
+}
+
+- (void)seekToFirst
+{
+ _iter->SeekToFirst();
+}
+
+- (void)seekToLast
+{
+ _iter->SeekToLast();
+}
+
+- (NSString *)nextKey
+{
+ _iter->Next();
+ return [self key];
+}
+
+- (NSString *)key
+{
+ if (_iter->Valid() == false)
+ return nil;
+ leveldb::Slice value = _iter->key();
+ return StringFromSlice(value);
+}
+
+- (NSString *)valueAsString
+{
+ if (_iter->Valid() == false)
+ return nil;
+ leveldb::Slice value = _iter->value();
+ return StringFromSlice(value);
+}
+
+- (NSData *)valueAsData
+{
+ if (_iter->Valid() == false)
+ return nil;
+ leveldb::Slice value = _iter->value();
+ return [NSData dataWithBytes:value.data() length:value.size()];
+}
+
+@end
+
+
+
+#pragma mark - APLevelDBWriteBatch
+
+@implementation APLevelDBWriteBatch
+
+- (id)initWithLevelDB:(APLevelDB *)levelDB {
+ self = [super init];
+ if (self != nil) {
+ self->_levelDB = levelDB;
+ }
+ return self;
+}
+
+- (void)setData:(NSData *)data forKey:(NSString *)key
+{
+ leveldb::Slice keySlice = SliceFromString(key);
+ leveldb::Slice valueSlice = leveldb::Slice((const char *)[data bytes], (size_t)[data length]);
+ _batch.Put(keySlice, valueSlice);
+}
+- (void)setString:(NSString *)str forKey:(NSString *)key
+{
+ leveldb::Slice keySlice = SliceFromString(key);
+ leveldb::Slice valueSlice = SliceFromString(str);
+ _batch.Put(keySlice, valueSlice);
+}
+
+- (void)removeKey:(NSString *)key
+{
+ leveldb::Slice keySlice = SliceFromString(key);
+ _batch.Delete(keySlice);
+}
+
+- (void)clear
+{
+ _batch.Clear();
+}
+
+- (BOOL)commit {
+ return [self.levelDB commitWriteBatch:self];
+}
+
+@end
+
diff --git a/Firebase/Firebase/Firebase.h b/Firebase/Firebase/Firebase.h
new file mode 100644
index 0000000..f74e49f
--- /dev/null
+++ b/Firebase/Firebase/Firebase.h
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#if !defined(__has_include)
+ #error "Firebase.h won't import anything if your compiler doesn't support __has_include. Please \
+ import the headers individually."
+#else
+ #if __has_include(<Firebase/FirebaseCore.h>)
+ #import <Firebase/FirebaseCore.h>
+ #endif
+
+ #if __has_include(<FirebaseAnalytics/FirebaseAnalytics.h>)
+ #import <FirebaseAnalytics/FirebaseAnalytics.h>
+ #endif
+
+ #if __has_include(<FirebaseAppIndexing/FirebaseAppIndexing.h>)
+ #import <FirebaseAppIndexing/FirebaseAppIndexing.h>
+ #endif
+
+ #if __has_include(<Firebase/FirebaseAuth.h>)
+ #import <Firebase/FirebaseAuth.h>
+ #endif
+
+ #if __has_include(<FirebaseCrash/FirebaseCrash.h>)
+ #import <FirebaseCrash/FirebaseCrash.h>
+ #endif
+
+ #if __has_include(<Firebase/FirebaseDatabase.h>)
+ #import <Firebase/FirebaseDatabase.h>
+ #endif
+
+ #if __has_include(<FirebaseDynamicLinks/FirebaseDynamicLinks.h>)
+ #import <FirebaseDynamicLinks/FirebaseDynamicLinks.h>
+ #endif
+
+ #if __has_include(<Firebase/FirebaseInstanceID.h>)
+ #import <Firebase/FirebaseInstanceID.h>
+ #endif
+
+ #if __has_include(<FirebaseInvites/FirebaseInvites.h>)
+ #import <FirebaseInvites/FirebaseInvites.h>
+ #endif
+
+ #if __has_include(<Firebase/FirebaseMessaging.h>)
+ #import <Firebase/FirebaseMessaging.h>
+ #endif
+
+ #if __has_include(<FirebaseRemoteConfig/FirebaseRemoteConfig.h>)
+ #import <FirebaseRemoteConfig/FirebaseRemoteConfig.h>
+ #endif
+
+ #if __has_include(<Firebase/FirebaseStorage.h>)
+ #import <Firebase/FirebaseStorage.h>
+ #endif
+
+ #if __has_include(<GoogleMobileAds/GoogleMobileAds.h>)
+ #import <GoogleMobileAds/GoogleMobileAds.h>
+ #endif
+
+#endif // defined(__has_include)
diff --git a/Firebase/Firebase/module.modulemap b/Firebase/Firebase/module.modulemap
new file mode 100755
index 0000000..3685b54
--- /dev/null
+++ b/Firebase/Firebase/module.modulemap
@@ -0,0 +1,4 @@
+module Firebase {
+ export *
+ header "Firebase.h"
+} \ No newline at end of file
diff --git a/Firebase/Messaging/FIRMMessageCode.h b/Firebase/Messaging/FIRMMessageCode.h
new file mode 100644
index 0000000..dc381ee
--- /dev/null
+++ b/Firebase/Messaging/FIRMMessageCode.h
@@ -0,0 +1,169 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+typedef NS_ENUM(NSInteger, FIRMessagingMessageCode) {
+ // FIRMessaging+FIRApp.m
+ kFIRMessagingMessageCodeFIRApp000 = 1000, // I-FCM001000
+ // FIRMessaging.m
+ kFIRMessagingMessageCodeMessaging000 = 2000, // I-FCM002000
+ kFIRMessagingMessageCodeMessaging001 = 2001, // I-FCM002001
+ kFIRMessagingMessageCodeMessaging002 = 2002, // I-FCM002002 - no longer used
+ kFIRMessagingMessageCodeMessaging003 = 2003, // I-FCM002003
+ kFIRMessagingMessageCodeMessaging004 = 2004, // I-FCM002004
+ kFIRMessagingMessageCodeMessaging005 = 2005, // I-FCM002005
+ kFIRMessagingMessageCodeMessaging006 = 2006, // I-FCM002006 - no longer used
+ kFIRMessagingMessageCodeMessaging007 = 2007, // I-FCM002007 - no longer used
+ kFIRMessagingMessageCodeMessaging008 = 2008, // I-FCM002008 - no longer used
+ kFIRMessagingMessageCodeMessaging009 = 2009, // I-FCM002009
+ kFIRMessagingMessageCodeMessaging010 = 2010, // I-FCM002010
+ kFIRMessagingMessageCodeMessaging011 = 2011, // I-FCM002011
+ kFIRMessagingMessageCodeMessaging012 = 2012, // I-FCM002012
+ kFIRMessagingMessageCodeMessaging013 = 2013, // I-FCM002013
+ kFIRMessagingMessageCodeMessaging014 = 2014, // I-FCM002014
+ kFIRMessagingMessageCodeMessaging015 = 2015, // I-FCM002015
+ kFIRMessagingMessageCodeMessaging016 = 2016, // I-FCM002016
+ kFIRMessagingMessageCodeMessaging017 = 2017, // I-FCM002017
+ kFIRMessagingMessageCodeMessaging018 = 2018, // I-FCM002018
+ kFIRMessagingMessageCodeRemoteMessageDelegateMethodNotImplemented = 2019, // I-FCM002019
+ kFIRMessagingMessageCodeSenderIDNotSuppliedForTokenFetch = 2020, // I-FCM002020
+ kFIRMessagingMessageCodeSenderIDNotSuppliedForTokenDelete = 2021, // I-FCM002021
+ kFIRMessagingMessageCodeAPNSTokenNotAvailableDuringTokenFetch = 2022, // I-FCM002022
+ // FIRMessagingClient.m
+ kFIRMessagingMessageCodeClient000 = 4000, // I-FCM004000
+ kFIRMessagingMessageCodeClient001 = 4001, // I-FCM004001
+ kFIRMessagingMessageCodeClient002 = 4002, // I-FCM004002
+ kFIRMessagingMessageCodeClient003 = 4003, // I-FCM004003
+ kFIRMessagingMessageCodeClient004 = 4004, // I-FCM004004
+ kFIRMessagingMessageCodeClient005 = 4005, // I-FCM004005
+ kFIRMessagingMessageCodeClient006 = 4006, // I-FCM004006
+ kFIRMessagingMessageCodeClient007 = 4007, // I-FCM004007
+ kFIRMessagingMessageCodeClient008 = 4008, // I-FCM004008
+ kFIRMessagingMessageCodeClient009 = 4009, // I-FCM004009
+ kFIRMessagingMessageCodeClient010 = 4010, // I-FCM004010
+ kFIRMessagingMessageCodeClient011 = 4011, // I-FCM004011
+ // FIRMessagingConnection.m
+ kFIRMessagingMessageCodeConnection000 = 5000, // I-FCM005000
+ kFIRMessagingMessageCodeConnection001 = 5001, // I-FCM005001
+ kFIRMessagingMessageCodeConnection002 = 5002, // I-FCM005002
+ kFIRMessagingMessageCodeConnection003 = 5003, // I-FCM005003
+ kFIRMessagingMessageCodeConnection004 = 5004, // I-FCM005004
+ kFIRMessagingMessageCodeConnection005 = 5005, // I-FCM005005
+ kFIRMessagingMessageCodeConnection006 = 5006, // I-FCM005006
+ kFIRMessagingMessageCodeConnection007 = 5007, // I-FCM005007
+ kFIRMessagingMessageCodeConnection008 = 5008, // I-FCM005008
+ kFIRMessagingMessageCodeConnection009 = 5009, // I-FCM005009
+ kFIRMessagingMessageCodeConnection010 = 5010, // I-FCM005010
+ kFIRMessagingMessageCodeConnection011 = 5011, // I-FCM005011
+ kFIRMessagingMessageCodeConnection012 = 5012, // I-FCM005012
+ kFIRMessagingMessageCodeConnection013 = 5013, // I-FCM005013
+ kFIRMessagingMessageCodeConnection014 = 5014, // I-FCM005014
+ kFIRMessagingMessageCodeConnection015 = 5015, // I-FCM005015
+ kFIRMessagingMessageCodeConnection016 = 5016, // I-FCM005016
+ kFIRMessagingMessageCodeConnection017 = 5017, // I-FCM005017
+ kFIRMessagingMessageCodeConnection018 = 5018, // I-FCM005018
+ kFIRMessagingMessageCodeConnection019 = 5019, // I-FCM005019
+ kFIRMessagingMessageCodeConnection020 = 5020, // I-FCM005020
+ kFIRMessagingMessageCodeConnection021 = 5021, // I-FCM005021
+ kFIRMessagingMessageCodeConnection022 = 5022, // I-FCM005022
+ kFIRMessagingMessageCodeConnection023 = 5023, // I-FCM005023
+ // FIRMessagingContextManagerService.m
+ kFIRMessagingMessageCodeContextManagerService000 = 6000, // I-FCM006000
+ kFIRMessagingMessageCodeContextManagerService001 = 6001, // I-FCM006001
+ kFIRMessagingMessageCodeContextManagerService002 = 6002, // I-FCM006002
+ kFIRMessagingMessageCodeContextManagerService003 = 6003, // I-FCM006003
+ kFIRMessagingMessageCodeContextManagerService004 = 6004, // I-FCM006004
+ kFIRMessagingMessageCodeContextManagerService005 = 6005, // I-FCM006005
+ // FIRMessagingDataMessageManager.m
+ kFIRMessagingMessageCodeDataMessageManager000 = 7000, // I-FCM007000
+ kFIRMessagingMessageCodeDataMessageManager001 = 7001, // I-FCM007001
+ kFIRMessagingMessageCodeDataMessageManager002 = 7002, // I-FCM007002
+ kFIRMessagingMessageCodeDataMessageManager003 = 7003, // I-FCM007003
+ kFIRMessagingMessageCodeDataMessageManager004 = 7004, // I-FCM007004
+ kFIRMessagingMessageCodeDataMessageManager005 = 7005, // I-FCM007005
+ kFIRMessagingMessageCodeDataMessageManager006 = 7006, // I-FCM007006
+ kFIRMessagingMessageCodeDataMessageManager007 = 7007, // I-FCM007007
+ kFIRMessagingMessageCodeDataMessageManager008 = 7008, // I-FCM007008
+ kFIRMessagingMessageCodeDataMessageManager009 = 7009, // I-FCM007009
+ kFIRMessagingMessageCodeDataMessageManager010 = 7010, // I-FCM007010
+ kFIRMessagingMessageCodeDataMessageManager011 = 7011, // I-FCM007011
+ kFIRMessagingMessageCodeDataMessageManager012 = 7012, // I-FCM007012
+ // FIRMessagingPendingTopicsList.m
+ kFIRMessagingMessageCodePendingTopicsList000 = 8000, // I-FCM008000
+ // FIRMessagingPubSub.m
+ kFIRMessagingMessageCodePubSub000 = 9000, // I-FCM009000
+ kFIRMessagingMessageCodePubSub001 = 9001, // I-FCM009001
+ kFIRMessagingMessageCodePubSub002 = 9002, // I-FCM009002
+ kFIRMessagingMessageCodePubSub003 = 9003, // I-FCM009003
+ // FIRMessagingReceiver.m
+ kFIRMessagingMessageCodeReceiver000 = 10000, // I-FCM010000
+ kFIRMessagingMessageCodeReceiver001 = 10001, // I-FCM010001
+ kFIRMessagingMessageCodeReceiver002 = 10002, // I-FCM010002
+ kFIRMessagingMessageCodeReceiver003 = 10003, // I-FCM010003
+ kFIRMessagingMessageCodeReceiver004 = 10004, // I-FCM010004 - no longer used
+ kFIRMessagingMessageCodeReceiver005 = 10005, // I-FCM010005
+ // FIRMessagingRegistrar.m
+ kFIRMessagingMessageCodeRegistrar000 = 11000, // I-FCM011000
+ // FIRMessagingRemoteNotificationsProxy.m
+ kFIRMessagingMessageCodeRemoteNotificationsProxy000 = 12000, // I-FCM012000
+ kFIRMessagingMessageCodeRemoteNotificationsProxy001 = 12001, // I-FCM012001
+ kFIRMessagingMessageCodeRemoteNotificationsProxyAPNSFailed = 12002, // I-FCM012002
+ // FIRMessagingRmq2PersistentStore.m
+ kFIRMessagingMessageCodeRmq2PersistentStore000 = 13000, // I-FCM013000
+ kFIRMessagingMessageCodeRmq2PersistentStore001 = 13001, // I-FCM013001
+ kFIRMessagingMessageCodeRmq2PersistentStore002 = 13002, // I-FCM013002
+ kFIRMessagingMessageCodeRmq2PersistentStore003 = 13003, // I-FCM013003
+ kFIRMessagingMessageCodeRmq2PersistentStore004 = 13004, // I-FCM013004
+ kFIRMessagingMessageCodeRmq2PersistentStore005 = 13005, // I-FCM013005
+ kFIRMessagingMessageCodeRmq2PersistentStore006 = 13006, // I-FCM013006
+ // FIRMessagingRmqManager.m
+ kFIRMessagingMessageCodeRmqManager000 = 14000, // I-FCM014000
+ // FIRMessagingSecureSocket.m
+ kFIRMessagingMessageCodeSecureSocket000 = 15000, // I-FCM015000
+ kFIRMessagingMessageCodeSecureSocket001 = 15001, // I-FCM015001
+ kFIRMessagingMessageCodeSecureSocket002 = 15002, // I-FCM015002
+ kFIRMessagingMessageCodeSecureSocket003 = 15003, // I-FCM015003
+ kFIRMessagingMessageCodeSecureSocket004 = 15004, // I-FCM015004
+ kFIRMessagingMessageCodeSecureSocket005 = 15005, // I-FCM015005
+ kFIRMessagingMessageCodeSecureSocket006 = 15006, // I-FCM015006
+ kFIRMessagingMessageCodeSecureSocket007 = 15007, // I-FCM015007
+ kFIRMessagingMessageCodeSecureSocket008 = 15008, // I-FCM015008
+ kFIRMessagingMessageCodeSecureSocket009 = 15009, // I-FCM015009
+ kFIRMessagingMessageCodeSecureSocket010 = 15010, // I-FCM015010
+ kFIRMessagingMessageCodeSecureSocket011 = 15011, // I-FCM015011
+ kFIRMessagingMessageCodeSecureSocket012 = 15012, // I-FCM015012
+ kFIRMessagingMessageCodeSecureSocket013 = 15013, // I-FCM015013
+ kFIRMessagingMessageCodeSecureSocket014 = 15014, // I-FCM015014
+ kFIRMessagingMessageCodeSecureSocket015 = 15015, // I-FCM015015
+ kFIRMessagingMessageCodeSecureSocket016 = 15016, // I-FCM015016
+ // FIRMessagingSyncMessageManager.m
+ kFIRMessagingMessageCodeSyncMessageManager000 = 16000, // I-FCM016000
+ kFIRMessagingMessageCodeSyncMessageManager001 = 16001, // I-FCM016001
+ kFIRMessagingMessageCodeSyncMessageManager002 = 16002, // I-FCM016002
+ kFIRMessagingMessageCodeSyncMessageManager003 = 16003, // I-FCM016003
+ kFIRMessagingMessageCodeSyncMessageManager004 = 16004, // I-FCM016004
+ kFIRMessagingMessageCodeSyncMessageManager005 = 16005, // I-FCM016005
+ kFIRMessagingMessageCodeSyncMessageManager006 = 16006, // I-FCM016006
+ kFIRMessagingMessageCodeSyncMessageManager007 = 16007, // I-FCM016007
+ kFIRMessagingMessageCodeSyncMessageManager008 = 16008, // I-FCM016008
+ // FIRMessagingTopicOperation.m
+ kFIRMessagingMessageCodeTopicOption000 = 17000, // I-FCM017000
+ kFIRMessagingMessageCodeTopicOption001 = 17001, // I-FCM017001
+ kFIRMessagingMessageCodeTopicOption002 = 17002, // I-FCM017002
+ // FIRMessagingUtilities.m
+ kFIRMessagingMessageCodeUtilities000 = 18000, // I-FCM018000
+ kFIRMessagingMessageCodeUtilities001 = 18001, // I-FCM018001
+ kFIRMessagingMessageCodeUtilities002 = 18002, // I-FCM018002
+};
diff --git a/Firebase/Messaging/FIRMessaging+FIRApp.h b/Firebase/Messaging/FIRMessaging+FIRApp.h
new file mode 100644
index 0000000..743b0f4
--- /dev/null
+++ b/Firebase/Messaging/FIRMessaging+FIRApp.h
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRMessaging.h"
+
+/**
+ * This category extends FIRMessaging with the configuration for using Cloud Messaging.
+ */
+@interface FIRMessaging (FIRApp)
+
+@end
diff --git a/Firebase/Messaging/FIRMessaging+FIRApp.m b/Firebase/Messaging/FIRMessaging+FIRApp.m
new file mode 100644
index 0000000..fc53286
--- /dev/null
+++ b/Firebase/Messaging/FIRMessaging+FIRApp.m
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRMessaging+FIRApp.h"
+
+#import "FIRAppInternal.h"
+#import "FIROptionsInternal.h"
+
+#import "FIRMessagingConfig.h"
+#import "FIRMessagingConstants.h"
+#import "FIRMessagingLogger.h"
+#import "FIRMessagingPubSub.h"
+#import "FIRMessagingRemoteNotificationsProxy.h"
+#import "FIRMessagingVersionUtilities.h"
+#import "FIRMessaging_Private.h"
+
+@interface FIRMessaging ()
+
+@property(nonatomic, readwrite, strong) NSString *fcmSenderID;
+
+@end
+
+@implementation FIRMessaging (FIRApp)
+
++ (void)load {
+ // FIRMessaging by default removes itself from observing any notifications.
+ [[NSNotificationCenter defaultCenter] addObserver:self
+ selector:@selector(didReceiveConfigureSDKNotification:)
+ name:kFIRAppReadyToConfigureSDKNotification
+ object:[FIRApp class]];
+}
+
++ (void)didReceiveConfigureSDKNotification:(NSNotification *)notification {
+ NSDictionary *appInfoDict = notification.userInfo;
+ NSString *appName = appInfoDict[kFIRAppNameKey];
+ FIRApp *app = [FIRApp appNamed:appName];
+ [[FIRMessaging messaging] configureMessaging:app];
+}
+
+- (void)configureMessaging:(FIRApp *)app {
+ FIROptions *options = app.options;
+ NSError *error;
+ if (!options.GCMSenderID.length) {
+ error =
+ [FIRApp errorForSubspecConfigurationFailureWithDomain:kFirebaseCloudMessagingErrorDomain
+ errorCode:FIRErrorCodeCloudMessagingFailed
+ service:kFIRServiceMessaging
+ reason:@"Google Sender ID must not be nil"
+ @" or empty."];
+ [self exitApp:app withError:error];
+ return;
+ }
+
+ self.fcmSenderID = [options.GCMSenderID copy];
+
+ // Swizzle remote-notification-related methods (app delegate and UNUserNotificationCenter)
+ if ([FIRMessagingRemoteNotificationsProxy canSwizzleMethods]) {
+ FIRMessagingLoggerNotice(kFIRMessagingMessageCodeFIRApp000,
+ @"FIRMessaging Remote Notifications proxy enabled, will swizzle "
+ @"remote notification receiver handlers. Add \"%@\" to your "
+ @"Info.plist and set it to NO",
+ kFIRMessagingRemoteNotificationsProxyEnabledInfoPlistKey);
+ [FIRMessagingRemoteNotificationsProxy swizzleMethods];
+ }
+}
+
+- (void)exitApp:(FIRApp *)app withError:(NSError *)error {
+ [app sendLogsWithServiceName:kFIRServiceMessaging
+ version:FIRMessagingCurrentLibraryVersion()
+ error:error];
+ if (error) {
+ NSString *message = nil;
+ if (app.options.usingOptionsFromDefaultPlist) {
+ // Configured using plist file
+ message = [NSString stringWithFormat:@"Firebase Messaging has stopped your project because "
+ @"there are missing or incorrect values provided in %@.%@ that may prevent "
+ @"your app from behaving as expected:\n\n"
+ @"Error: %@\n\n"
+ @"Please fix these issues to ensure that Firebase is correctly configured in "
+ @"your project.",
+ kServiceInfoFileName,
+ kServiceInfoFileType,
+ error.localizedFailureReason];
+ } else {
+ // Configured manually
+ message = [NSString stringWithFormat:@"Firebase Messaging has stopped your project because "
+ @"there are missing or incorrect values in Firebase's configuration options "
+ @"that may prevent your app from behaving as expected:\n\n"
+ @"Error:%@\n\n"
+ @"Please fix these issues to ensure that Firebase is correctly configured in "
+ @"your project.",
+ error.localizedFailureReason];
+ }
+ [NSException raise:kFirebaseCloudMessagingErrorDomain format:@"%@", message];
+ }
+}
+
+@end
diff --git a/Firebase/Messaging/FIRMessaging.m b/Firebase/Messaging/FIRMessaging.m
new file mode 100644
index 0000000..94347c8
--- /dev/null
+++ b/Firebase/Messaging/FIRMessaging.m
@@ -0,0 +1,1071 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#if !__has_feature(objc_arc)
+#error FIRMessagingLib should be compiled with ARC.
+#endif
+
+#import "FIRMessaging.h"
+#import "FIRMessaging_Private.h"
+
+#import <UIKit/UIKit.h>
+
+#import "FIRMessagingClient.h"
+#import "FIRMessagingConfig.h"
+#import "FIRMessagingConstants.h"
+#import "FIRMessagingContextManagerService.h"
+#import "FIRMessagingDataMessageManager.h"
+#import "FIRMessagingDefines.h"
+#import "FIRMessagingInstanceIDProxy.h"
+#import "FIRMessagingLogger.h"
+#import "FIRMessagingPubSub.h"
+#import "FIRMessagingReceiver.h"
+#import "FIRMessagingRmqManager.h"
+#import "FIRMessagingSyncMessageManager.h"
+#import "FIRMessagingUtilities.h"
+#import "FIRMessagingVersionUtilities.h"
+
+#import "FIRReachabilityChecker.h"
+
+#import "NSError+FIRMessaging.h"
+
+static NSString *const kFIRMessagingMessageViaAPNSRootKey = @"aps";
+static NSString *const kFIRMessagingReachabilityHostname = @"www.google.com";
+static NSString *const kFIRMessagingDefaultTokenScope = @"*";
+static NSString *const kFIRMessagingFCMTokenFetchAPNSOption = @"apns_token";
+
+#if defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0
+const NSNotificationName FIRMessagingSendSuccessNotification =
+ @"com.firebase.messaging.notif.send-success";
+const NSNotificationName FIRMessagingSendErrorNotification =
+ @"com.firebase.messaging.notif.send-error";
+const NSNotificationName FIRMessagingMessagesDeletedNotification =
+ @"com.firebase.messaging.notif.messages-deleted";
+const NSNotificationName FIRMessagingConnectionStateChangedNotification =
+ @"com.firebase.messaging.notif.connection-state-changed";
+const NSNotificationName FIRMessagingRegistrationTokenRefreshedNotification =
+ @"com.firebase.messaging.notif.fcm-token-refreshed";
+#else
+NSString *const FIRMessagingSendSuccessNotification =
+ @"com.firebase.messaging.notif.send-success";
+NSString *const FIRMessagingSendErrorNotification =
+ @"com.firebase.messaging.notif.send-error";
+NSString * const FIRMessagingMessagesDeletedNotification =
+ @"com.firebase.messaging.notif.messages-deleted";
+NSString * const FIRMessagingConnectionStateChangedNotification =
+ @"com.firebase.messaging.notif.connection-state-changed";
+NSString * const FIRMessagingRegistrationTokenRefreshedNotification =
+ @"com.firebase.messaging.notif.fcm-token-refreshed";
+#endif // defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0
+
+// Copied from Apple's header in case it is missing in some cases (e.g. pre-Xcode 8 builds).
+#ifndef NSFoundationVersionNumber_iOS_8_x_Max
+#define NSFoundationVersionNumber_iOS_8_x_Max 1199
+#endif
+
+@interface FIRMessagingMessageInfo ()
+
+@property(nonatomic, readwrite, assign) FIRMessagingMessageStatus status;
+
+@end
+
+@implementation FIRMessagingMessageInfo
+
+- (instancetype)init {
+ FIRMessagingInvalidateInitializer();
+}
+
+- (instancetype)initWithStatus:(FIRMessagingMessageStatus)status {
+ self = [super init];
+ if (self) {
+ _status = status;
+ }
+ return self;
+}
+
+@end
+
+#pragma mark - for iOS 10 compatibility
+@implementation FIRMessagingRemoteMessage
+
+- (instancetype)init {
+ self = [super init];
+ if (self) {
+ _appData = [[NSMutableDictionary alloc] init];
+ }
+
+ return self;
+}
+
+- (instancetype)initWithMessage:(FIRMessagingRemoteMessage *)message {
+ self = [self init];
+ if (self) {
+ _appData = [message.appData copy];
+ }
+
+ return self;
+}
+
+@end
+
+@interface FIRMessaging ()
+ <FIRMessagingClientDelegate, FIRMessagingReceiverDelegate, FIRReachabilityDelegate>
+
+// FIRApp properties
+@property(nonatomic, readwrite, copy) NSString *fcmSenderID;
+@property(nonatomic, readwrite, strong) NSData *apnsTokenData;
+@property(nonatomic, readwrite, strong) NSString *defaultFcmToken;
+
+// This object is used as a proxy for reflection-based calls to FIRInstanceID.
+// Due to our packaging requirements, we can't directly depend on FIRInstanceID currently.
+@property(nonatomic, readwrite, strong) FIRMessagingInstanceIDProxy *instanceIDProxy;
+
+@property(nonatomic, readwrite, strong) FIRMessagingConfig *config;
+@property(nonatomic, readwrite, assign) BOOL isClientSetup;
+
+@property(nonatomic, readwrite, strong) FIRMessagingClient *client;
+@property(nonatomic, readwrite, strong) FIRReachabilityChecker *reachability;
+@property(nonatomic, readwrite, strong) FIRMessagingDataMessageManager *dataMessageManager;
+@property(nonatomic, readwrite, strong) FIRMessagingPubSub *pubsub;
+@property(nonatomic, readwrite, strong) FIRMessagingRmqManager *rmq2Manager;
+@property(nonatomic, readwrite, strong) FIRMessagingReceiver *receiver;
+@property(nonatomic, readwrite, strong) FIRMessagingSyncMessageManager *syncMessageManager;
+
+/// Message ID's logged for analytics. This prevents us from logging the same message twice
+/// which can happen if the user inadvertently calls `appDidReceiveMessage` along with us
+/// calling it implicitly during swizzling.
+@property(nonatomic, readwrite, strong) NSMutableSet *loggedMessageIDs;
+
+@end
+
+@implementation FIRMessaging
+
++ (FIRMessaging *)messaging {
+ static FIRMessaging *messaging;
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ // Start Messaging (Fully initialize in one place).
+ FIRMessagingConfig *config = [FIRMessagingConfig defaultConfig];
+ messaging = [[FIRMessaging alloc] initWithConfig:config];
+ [messaging start];
+ });
+ return messaging;
+}
+
+- (instancetype)initWithConfig:(FIRMessagingConfig *)config {
+ self = [super init];
+ if (self) {
+ _config = config;
+ _loggedMessageIDs = [NSMutableSet set];
+ _instanceIDProxy = [[FIRMessagingInstanceIDProxy alloc] init];
+ }
+ return self;
+}
+
+- (void)dealloc {
+ [self.reachability stop];
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
+ [self teardown];
+}
+
+- (void)setRemoteMessageDelegate:(id<FIRMessagingDelegate>)delegate {
+ _delegate = delegate;
+}
+
+- (id<FIRMessagingDelegate>)remoteMessageDelegate {
+ return self.delegate;
+}
+
+#pragma mark - Config
+
+- (void)start {
+ _FIRMessagingDevAssert(self.config, @"Invalid nil config in FIRMessagingService");
+
+ [self saveLibraryVersion];
+ [self setupLogger:self.config.logLevel];
+ [self setupReceiverWithConfig:self.config];
+
+ NSString *hostname = kFIRMessagingReachabilityHostname;
+ self.reachability = [[FIRReachabilityChecker alloc] initWithReachabilityDelegate:self
+ loggerDelegate:nil
+ withHost:hostname];
+ [self.reachability start];
+
+ [self setupApplicationSupportSubDirectory];
+ // setup FIRMessaging objects
+ [self setupRmqManager];
+ [self setupClient];
+ [self setupSyncMessageManager];
+ [self setupDataMessageManager];
+ [self setupTopics];
+
+ self.isClientSetup = YES;
+ [self setupNotificationListeners];
+}
+
+- (void)setupApplicationSupportSubDirectory {
+ NSString *messagingSubDirectory = kFIRMessagingApplicationSupportSubDirectory;
+ if (![[self class] hasApplicationSupportSubDirectory:messagingSubDirectory]) {
+ [[self class] createApplicationSupportSubDirectory:messagingSubDirectory];
+ }
+}
+
+- (void)setupNotificationListeners {
+ // To prevent multiple notifications remove self as observer for all events.
+ NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
+ [center removeObserver:self];
+
+ [center addObserver:self
+ selector:@selector(didReceiveDefaultInstanceIDToken:)
+ name:kFIRMessagingFCMTokenNotification
+ object:nil];
+ [center addObserver:self
+ selector:@selector(defaultInstanceIDTokenWasRefreshed:)
+ name:kFIRMessagingInstanceIDTokenRefreshNotification
+ object:nil];
+ [center addObserver:self
+ selector:@selector(didReceiveAPNSToken:)
+ name:kFIRMessagingAPNSTokenNotification
+ object:nil];
+
+ [center addObserver:self
+ selector:@selector(applicationStateChanged)
+ name:UIApplicationDidBecomeActiveNotification
+ object:nil];
+ [center addObserver:self
+ selector:@selector(applicationStateChanged)
+ name:UIApplicationDidEnterBackgroundNotification
+ object:nil];
+}
+
+- (void)saveLibraryVersion {
+ NSString *currentLibraryVersion = FIRMessagingCurrentLibraryVersion();
+ [[NSUserDefaults standardUserDefaults] setObject:currentLibraryVersion
+ forKey:kFIRMessagingLibraryVersion];
+ FIRMessagingLoggerInfo(kFIRMessagingMessageCodeMessaging000, @"FIRMessaging library version %@",
+ currentLibraryVersion);
+}
+
+- (void)setupLogger:(FIRMessagingLogLevel)loggerLevel {
+#if FIRMessaging_PROBER
+ // do nothing
+#else
+ FIRMessagingLogger *logger = FIRMessagingSharedLogger();
+ FIRMessagingLogLevelFilter *filter =
+ [[FIRMessagingLogLevelFilter alloc] initWithLevel:loggerLevel];
+ [logger setFilter:filter];
+#endif
+}
+
+- (void)setupReceiverWithConfig:(FIRMessagingConfig *)config {
+ self.receiver = [[FIRMessagingReceiver alloc] init];
+ self.receiver.delegate = self;
+}
+
+- (void)setupClient {
+ self.client = [[FIRMessagingClient alloc] initWithDelegate:self
+ reachability:self.reachability
+ rmq2Manager:self.rmq2Manager];
+}
+
+- (void)setupDataMessageManager {
+ self.dataMessageManager =
+ [[FIRMessagingDataMessageManager alloc] initWithDelegate:self.receiver
+ client:self.client
+ rmq2Manager:self.rmq2Manager
+ syncMessageManager:self.syncMessageManager];
+
+ [self.dataMessageManager refreshDelayedMessages];
+ [self.client setDataMessageManager:self.dataMessageManager];
+}
+
+- (void)setupRmqManager {
+ self.rmq2Manager = [[FIRMessagingRmqManager alloc] initWithDatabaseName:@"rmq2"];
+ [self.rmq2Manager loadRmqId];
+}
+
+- (void)setupTopics {
+ _FIRMessagingDevAssert(self.client, @"Invalid nil client before init pubsub.");
+ self.pubsub = [[FIRMessagingPubSub alloc] initWithClient:self.client];
+}
+
+- (void)setupSyncMessageManager {
+ self.syncMessageManager =
+ [[FIRMessagingSyncMessageManager alloc] initWithRmqManager:self.rmq2Manager];
+
+ // Delete the expired messages with a delay. We don't want to block startup with a somewhat
+ // expensive db call.
+ FIRMessaging_WEAKIFY(self);
+ dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5.0 * NSEC_PER_SEC));
+ dispatch_after(time, dispatch_get_main_queue(), ^{
+ FIRMessaging_STRONGIFY(self);
+ [self.syncMessageManager removeExpiredSyncMessages];
+ });
+}
+
+- (void)teardown {
+ _FIRMessagingDevAssert([NSThread isMainThread],
+ @"FIRMessaging should be called from main thread only.");
+ [self.client teardown];
+ self.pubsub = nil;
+ self.syncMessageManager = nil;
+ self.rmq2Manager = nil;
+ self.dataMessageManager = nil;
+ self.client = nil;
+ self.isClientSetup = NO;
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeMessaging001, @"Did successfully teardown");
+}
+
+#pragma mark - Messages
+
+- (FIRMessagingMessageInfo *)appDidReceiveMessage:(NSDictionary *)message {
+ if (!message.count) {
+ return [[FIRMessagingMessageInfo alloc] initWithStatus:FIRMessagingMessageStatusUnknown];
+ }
+
+ // For downstream messages that go via MCS we should strip out this key before sending
+ // the message to the device.
+ BOOL isOldMessage = NO;
+ NSString *messageID = message[kFIRMessagingMessageIDKey];
+ if ([messageID length]) {
+ [self.rmq2Manager saveS2dMessageWithRmqId:messageID];
+
+ BOOL isSyncMessage = [[self class] isAPNSSyncMessage:message];
+ if (isSyncMessage) {
+ isOldMessage = [self.syncMessageManager didReceiveAPNSSyncMessage:message];
+ }
+ }
+ // Prevent duplicates by keeping a cache of all the logged messages during each session.
+ // The duplicates only happen when the 3P app calls `appDidReceiveMessage:` along with
+ // us swizzling their implementation to call the same method implicitly.
+ if (!isOldMessage && messageID.length) {
+ isOldMessage = [self.loggedMessageIDs containsObject:messageID];
+ if (!isOldMessage) {
+ [self.loggedMessageIDs addObject:messageID];
+ }
+ }
+
+ if (!isOldMessage) {
+ Class firMessagingLogClass = NSClassFromString(@"FIRMessagingLog");
+ SEL logMessageSelector = NSSelectorFromString(@"logMessage:");
+
+ if ([firMessagingLogClass respondsToSelector:logMessageSelector]) {
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
+ [firMessagingLogClass performSelector:logMessageSelector
+ withObject:message];
+ }
+#pragma clang diagnostic pop
+ [self handleContextManagerMessage:message];
+ [self handleIncomingLinkIfNeededFromMessage:message];
+ }
+ return [[FIRMessagingMessageInfo alloc] initWithStatus:FIRMessagingMessageStatusNew];
+}
+
+- (BOOL)handleContextManagerMessage:(NSDictionary *)message {
+ if ([FIRMessagingContextManagerService isContextManagerMessage:message]) {
+ return [FIRMessagingContextManagerService handleContextManagerMessage:message];
+ }
+ return NO;
+}
+
++ (BOOL)isAPNSSyncMessage:(NSDictionary *)message {
+ if ([message[kFIRMessagingMessageViaAPNSRootKey] isKindOfClass:[NSDictionary class]]) {
+ NSDictionary *aps = message[kFIRMessagingMessageViaAPNSRootKey];
+ return [aps[kFIRMessagingMessageAPNSContentAvailableKey] boolValue];
+ }
+ return NO;
+}
+
+- (void)handleIncomingLinkIfNeededFromMessage:(NSDictionary *)message {
+ NSURL *url = [self linkURLFromMessage:message];
+ if (url == nil) {
+ return;
+ }
+ if (![NSThread isMainThread]) {
+ dispatch_async(dispatch_get_main_queue(), ^{
+ [self handleIncomingLinkIfNeededFromMessage:message];
+
+ });
+ return;
+ }
+ UIApplication *application = [UIApplication sharedApplication];
+ id<UIApplicationDelegate> appDelegate = application.delegate;
+ SEL continueUserActivitySelector =
+ @selector(application:continueUserActivity:restorationHandler:);
+ SEL openURLWithOptionsSelector = @selector(application:openURL:options:);
+ SEL openURLWithSourceApplicationSelector =
+ @selector(application:openURL:sourceApplication:annotation:);
+ SEL handleOpenURLSelector = @selector(application:handleOpenURL:);
+ // Due to FIRAAppDelegateProxy swizzling, this selector will most likely get chosen, whether or
+ // not the actual application has implemented
+ // |application:continueUserActivity:restorationHandler:|. A warning will be displayed to the user
+ // if they haven't implemented it.
+ if ([NSUserActivity class] != nil &&
+ [appDelegate respondsToSelector:continueUserActivitySelector]) {
+ NSUserActivity *userActivity =
+ [[NSUserActivity alloc] initWithActivityType:NSUserActivityTypeBrowsingWeb];
+ userActivity.webpageURL = url;
+ [appDelegate application:application
+ continueUserActivity:userActivity
+ restorationHandler:^(NSArray * _Nullable restorableObjects) {
+ // Do nothing, as we don't support the app calling this block
+ }];
+
+ } else if ([appDelegate respondsToSelector:openURLWithOptionsSelector]) {
+ [appDelegate application:application openURL:url options:@{}];
+
+ // Similarly, |application:openURL:sourceApplication:annotation:| will also always be called, due
+ // to the default swizzling done by FIRAAppDelegateProxy in Firebase Analytics
+ } else if ([appDelegate respondsToSelector:openURLWithSourceApplicationSelector]) {
+ [appDelegate application:application
+ openURL:url
+ sourceApplication:FIRMessagingAppIdentifier()
+ annotation:@{}];
+
+ } else if ([appDelegate respondsToSelector:handleOpenURLSelector]) {
+ [appDelegate application:application handleOpenURL:url];
+ }
+}
+
+- (NSURL *)linkURLFromMessage:(NSDictionary *)message {
+ NSString *urlString = message[kFIRMessagingMessageLinkKey];
+ if (urlString == nil || ![urlString isKindOfClass:[NSString class]] || urlString.length == 0) {
+ return nil;
+ }
+ NSURL *url = [NSURL URLWithString:urlString];
+ return url;
+}
+
+#pragma mark - APNS
+
+- (NSData *)APNSToken {
+ return self.apnsTokenData;
+}
+
+- (void)setAPNSToken:(NSData *)APNSToken {
+ [self setAPNSToken:APNSToken type:FIRMessagingAPNSTokenTypeUnknown];
+}
+
+- (void)setAPNSToken:(NSData *)apnsToken type:(FIRMessagingAPNSTokenType)type {
+ if ([apnsToken isEqual:self.apnsTokenData]) {
+ return;
+ }
+ self.apnsTokenData = apnsToken;
+ [self.instanceIDProxy setAPNSToken:apnsToken type:(FIRMessagingInstanceIDProxyAPNSTokenType)type];
+}
+
+#pragma mark - FCM
+
+- (NSString *)FCMToken {
+ NSString *token = self.defaultFcmToken;
+ if (!token) {
+ // We may not have received it from Instance ID yet (via NSNotification), so extract it directly
+ token = [self.instanceIDProxy token];
+ }
+ return token;
+}
+
+- (void)retrieveFCMTokenForSenderID:(nonnull NSString *)senderID
+ completion:(nonnull FIRMessagingFCMTokenFetchCompletion)completion {
+ if (!senderID.length) {
+ FIRMessagingLoggerError(kFIRMessagingMessageCodeSenderIDNotSuppliedForTokenFetch,
+ @"Sender ID not supplied. It is required for a token fetch, "
+ @"to identify the sender.");
+ if (completion) {
+ NSString *description = @"Couldn't fetch token because a Sender ID was not supplied. A valid "
+ @"Sender ID is required to fetch an FCM token";
+ NSError *error = [NSError fcm_errorWithCode:FIRMessagingErrorInvalidRequest
+ userInfo:@{NSLocalizedDescriptionKey : description}];
+ completion(nil, error);
+ }
+ return;
+ }
+ NSDictionary *options = nil;
+ if (self.APNSToken) {
+ options = @{kFIRMessagingFCMTokenFetchAPNSOption : self.APNSToken};
+ } else {
+ FIRMessagingLoggerWarn(kFIRMessagingMessageCodeAPNSTokenNotAvailableDuringTokenFetch,
+ @"APNS device token not set before retrieving FCM Token for Sender ID "
+ @"'%@'. Notifications to this FCM Token will not be delivered over APNS."
+ @"Be sure to re-retrieve the FCM token once the APNS device token is "
+ @"set.", senderID);
+ }
+ [self.instanceIDProxy tokenWithAuthorizedEntity:senderID
+ scope:kFIRMessagingDefaultTokenScope
+ options:options
+ handler:completion];
+}
+
+- (void)deleteFCMTokenForSenderID:(nonnull NSString *)senderID
+ completion:(nonnull FIRMessagingDeleteFCMTokenCompletion)completion {
+ if (!senderID.length) {
+ FIRMessagingLoggerError(kFIRMessagingMessageCodeSenderIDNotSuppliedForTokenDelete,
+ @"Sender ID not supplied. It is required to delete an FCM token.");
+ if (completion) {
+ NSString *description = @"Couldn't delete token because a Sender ID was not supplied. A "
+ @"valid Sender ID is required to delete an FCM token";
+ NSError *error = [NSError fcm_errorWithCode:FIRMessagingErrorInvalidRequest
+ userInfo:@{NSLocalizedDescriptionKey : description}];
+ completion(error);
+ }
+ return;
+ }
+ [self.instanceIDProxy deleteTokenWithAuthorizedEntity:senderID
+ scope:kFIRMessagingDefaultTokenScope
+ handler:completion];
+}
+
+#pragma mark - Application State Changes
+
+- (void)applicationStateChanged {
+ if (self.shouldEstablishDirectChannel) {
+ [self updateAutomaticClientConnection];
+ }
+}
+
+#pragma mark - Direct Channel
+
+- (void)setShouldEstablishDirectChannel:(BOOL)shouldEstablishDirectChannel {
+ if (_shouldEstablishDirectChannel == shouldEstablishDirectChannel) {
+ return;
+ }
+ _shouldEstablishDirectChannel = shouldEstablishDirectChannel;
+ [self updateAutomaticClientConnection];
+}
+
+- (BOOL)isDirectChannelEstablished {
+ return self.client.isConnectionActive;
+}
+
+- (BOOL)shouldBeConnectedAutomatically {
+ // We require a token from Instance ID
+ NSString *token = self.defaultFcmToken;
+ // Only on foreground connections
+ UIApplicationState applicationState = [UIApplication sharedApplication].applicationState;
+ BOOL shouldBeConnected = _shouldEstablishDirectChannel &&
+ (token.length > 0) &&
+ applicationState == UIApplicationStateActive;
+ return shouldBeConnected;
+}
+
+- (void)updateAutomaticClientConnection {
+ BOOL shouldBeConnected = [self shouldBeConnectedAutomatically];
+ if (shouldBeConnected && !self.client.isConnected) {
+ [self.client connectWithHandler:^(NSError *error) {
+ if (!error) {
+ // It means we connected. Fire connection change notification
+ [self notifyOfDirectChannelConnectionChange];
+ }
+ }];
+ } else if (!shouldBeConnected && self.client.isConnected) {
+ [self.client disconnect];
+ [self notifyOfDirectChannelConnectionChange];
+ }
+}
+
+- (void)notifyOfDirectChannelConnectionChange {
+ NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
+ [center postNotificationName:FIRMessagingConnectionStateChangedNotification object:self];
+}
+
+#pragma mark - Connect
+
+- (void)connectWithCompletion:(FIRMessagingConnectCompletion)handler {
+ _FIRMessagingDevAssert([NSThread isMainThread],
+ @"FIRMessaging connect should be called from main thread only.");
+ _FIRMessagingDevAssert(self.isClientSetup, @"FIRMessaging client not setup.");
+ [self.client connectWithHandler:^(NSError *error) {
+ if (handler) {
+ handler(error);
+ }
+ if (!error) {
+ // It means we connected. Fire connection change notification
+ [self notifyOfDirectChannelConnectionChange];
+ }
+ }];
+
+}
+
+- (void)disconnect {
+ _FIRMessagingDevAssert([NSThread isMainThread],
+ @"FIRMessaging should be called from main thread only.");
+ if ([self.client isConnected]) {
+ [self.client disconnect];
+ [self notifyOfDirectChannelConnectionChange];
+ }
+}
+
+#pragma mark - Topics
+
++ (NSString *)normalizeTopic:(NSString *)topic {
+ if (![FIRMessagingPubSub hasTopicsPrefix:topic]) {
+ topic = [FIRMessagingPubSub addPrefixToTopic:topic];
+ }
+ if ([FIRMessagingPubSub isValidTopicWithPrefix:topic]) {
+ return [topic copy];
+ }
+ return nil;
+}
+
+- (void)subscribeToTopic:(NSString *)topic {
+ if (self.defaultFcmToken.length && topic.length) {
+ NSString *normalizeTopic = [[self class ] normalizeTopic:topic];
+ if (normalizeTopic.length) {
+ [self.pubsub subscribeToTopic:normalizeTopic];
+ } else {
+ FIRMessagingLoggerError(kFIRMessagingMessageCodeMessaging009,
+ @"Cannot parse topic name %@. Will not subscribe.", topic);
+ }
+ } else {
+ FIRMessagingLoggerError(kFIRMessagingMessageCodeMessaging010,
+ @"Cannot subscribe to topic: %@ with token: %@", topic,
+ self.defaultFcmToken);
+ }
+}
+
+- (void)unsubscribeFromTopic:(NSString *)topic {
+ if (self.defaultFcmToken.length && topic.length) {
+ NSString *normalizeTopic = [[self class] normalizeTopic:topic];
+ if (normalizeTopic.length) {
+ [self.pubsub unsubscribeFromTopic:normalizeTopic];
+ } else {
+ FIRMessagingLoggerError(kFIRMessagingMessageCodeMessaging011,
+ @"Cannot parse topic name %@. Will not unsubscribe.", topic);
+ }
+ } else {
+ FIRMessagingLoggerError(kFIRMessagingMessageCodeMessaging012,
+ @"Cannot unsubscribe to topic: %@ with token: %@", topic,
+ self.defaultFcmToken);
+ }
+}
+
+#pragma mark - Send
+
+- (void)sendMessage:(NSDictionary *)message
+ to:(NSString *)to
+ withMessageID:(NSString *)messageID
+ timeToLive:(int64_t)ttl {
+ _FIRMessagingDevAssert([to length] != 0, @"Invalid receiver id for FIRMessaging-message");
+
+ NSMutableDictionary *fcmMessage = [[self class] createFIRMessagingMessageWithMessage:message
+ to:to
+ withID:messageID
+ timeToLive:ttl
+ delay:0];
+ FIRMessagingLoggerInfo(kFIRMessagingMessageCodeMessaging013, @"Sending message: %@ with id: %@",
+ message, messageID);
+ [self.dataMessageManager sendDataMessageStanza:fcmMessage];
+}
+
++ (NSMutableDictionary *)createFIRMessagingMessageWithMessage:(NSDictionary *)message
+ to:(NSString *)to
+ withID:(NSString *)msgID
+ timeToLive:(int64_t)ttl
+ delay:(int)delay {
+ NSMutableDictionary *fcmMessage = [NSMutableDictionary dictionary];
+ fcmMessage[kFIRMessagingSendTo] = [to copy];
+ fcmMessage[kFIRMessagingSendMessageID] = msgID ? [msgID copy] : @"";
+ fcmMessage[kFIRMessagingSendTTL] = @(ttl);
+ fcmMessage[kFIRMessagingSendDelay] = @(delay);
+ fcmMessage[KFIRMessagingSendMessageAppData] =
+ [NSMutableDictionary dictionaryWithDictionary:message];
+ return fcmMessage;
+}
+
+#pragma mark - IID dependencies
+
+// FIRMessagingInternalUtilities.h to see usage.
++ (NSString *)FIRMessagingSDKVersion {
+ NSString *semanticVersion = FIRMessagingCurrentLibraryVersion();
+ // Use prefix fcm for all FCM libs. This allows us to differentiate b/w
+ // the new and old FCM registrations.
+ return [NSString stringWithFormat:@"fcm-%@", semanticVersion];
+}
+
++ (NSString *)FIRMessagingSDKCurrentLocale {
+ return [self currentLocale];
+}
+
+- (void)setAPNSToken:(NSData *)apnsToken error:(NSError *)error {
+ if (apnsToken) {
+ self.apnsTokenData = [apnsToken copy];
+ }
+}
+
+#pragma mark - FIRMessagingReceiverDelegate
+
+- (void)receiver:(FIRMessagingReceiver *)receiver
+ receivedRemoteMessage:(FIRMessagingRemoteMessage *)remoteMessage {
+ if ([self.delegate respondsToSelector:@selector(messaging:didReceiveMessage:)]) {
+ [self.delegate messaging:self didReceiveMessage:remoteMessage];
+ } else if ([self.delegate respondsToSelector:@selector(applicationReceivedRemoteMessage:)]) {
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdeprecated-declarations"
+ [self.delegate applicationReceivedRemoteMessage:remoteMessage];
+#pragma clang diagnostic pop
+ } else {
+ // Delegate methods weren't implemented, so messages are being dropped, log a warning
+ FIRMessagingLoggerWarn(kFIRMessagingMessageCodeRemoteMessageDelegateMethodNotImplemented,
+ @"FIRMessaging received data-message, but FIRMessagingDelegate's"
+ @"-messaging:didReceiveMessage: not implemented");
+ }
+}
+
+#pragma mark - FIRReachabilityDelegate
+
+- (void)reachability:(FIRReachabilityChecker *)reachability
+ statusChanged:(FIRReachabilityStatus)status {
+ [self onNetworkStatusChanged];
+}
+
+#pragma mark - Network
+
+- (BOOL)isNetworkAvailable {
+ FIRReachabilityStatus status = self.reachability.reachabilityStatus;
+ return (status == kFIRReachabilityViaCellular || status == kFIRReachabilityViaWifi);
+}
+
+- (FIRMessagingNetworkStatus)networkType {
+ FIRReachabilityStatus status = self.reachability.reachabilityStatus;
+ if (![self isNetworkAvailable]) {
+ return kFIRMessagingReachabilityNotReachable;
+ } else if (status == kFIRReachabilityViaCellular) {
+ return kFIRMessagingReachabilityReachableViaWWAN;
+ } else {
+ return kFIRMessagingReachabilityReachableViaWiFi;
+ }
+}
+
+#pragma mark - Notifications
+
+- (void)onNetworkStatusChanged {
+ if (![self.client isConnected] && [self isNetworkAvailable]) {
+ if (self.client.shouldStayConnected) {
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeMessaging014,
+ @"Attempting to establish direct channel.");
+ [self.client retryConnectionImmediately:YES];
+ }
+ [self.pubsub scheduleSync:YES];
+ }
+}
+
+#pragma mark - Notifications
+
+- (void)didReceiveDefaultInstanceIDToken:(NSNotification *)notification {
+ if (![notification.object isKindOfClass:[NSString class]]) {
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeMessaging015,
+ @"Invalid default FCM token type %@",
+ NSStringFromClass([notification.object class]));
+ return;
+ }
+ self.defaultFcmToken = [(NSString *)notification.object copy];
+ [self.pubsub scheduleSync:YES];
+ if (self.shouldEstablishDirectChannel) {
+ [self updateAutomaticClientConnection];
+ }
+}
+
+- (void)defaultInstanceIDTokenWasRefreshed:(NSNotification *)notification {
+ // Retrieve the Instance ID default token, and if it is non-nil, post it
+ NSString *token = [self.instanceIDProxy token];
+ // Sometimes Instance ID doesn't yet have a token, so wait until the default
+ // token is fetched, and then notify. This ensures that this token should not
+ // be nil when the developer accesses it.
+ if (token != nil) {
+ self.defaultFcmToken = [token copy];
+ [self.delegate messaging:self didRefreshRegistrationToken:token];
+ NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
+ [center postNotificationName:FIRMessagingRegistrationTokenRefreshedNotification object:nil];
+ }
+}
+
+- (void)didReceiveAPNSToken:(NSNotification *)notification {
+ NSData *apnsToken = notification.object;
+ if (![apnsToken isKindOfClass:[NSData class]]) {
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeMessaging016, @"Invalid APNS token type %@",
+ NSStringFromClass([notification.object class]));
+ return;
+ }
+ // Set this value directly, and since this came from InstanceID, don't set it back to InstanceID
+ self.apnsTokenData = [apnsToken copy];
+}
+
+#pragma mark - Application Support Directory
+
++ (BOOL)hasApplicationSupportSubDirectory:(NSString *)subDirectoryName {
+ NSString *subDirectoryPath = [self pathForApplicationSupportSubDirectory:subDirectoryName];
+ BOOL isDirectory;
+ if (![[NSFileManager defaultManager] fileExistsAtPath:subDirectoryPath
+ isDirectory:&isDirectory]) {
+ return NO;
+ } else if (!isDirectory) {
+ return NO;
+ }
+ return YES;
+}
+
++ (NSString *)pathForApplicationSupportSubDirectory:(NSString *)subDirectoryName {
+ NSArray *directoryPaths = NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory,
+ NSUserDomainMask, YES);
+ NSString *applicationSupportDirPath = directoryPaths.lastObject;
+ NSArray *components = @[applicationSupportDirPath, subDirectoryName];
+ return [NSString pathWithComponents:components];
+}
+
++ (BOOL)createApplicationSupportSubDirectory:(NSString *)subDirectoryName {
+ NSString *subDirectoryPath = [self pathForApplicationSupportSubDirectory:subDirectoryName];
+ BOOL hasSubDirectory;
+
+ if (![[NSFileManager defaultManager] fileExistsAtPath:subDirectoryPath
+ isDirectory:&hasSubDirectory]) {
+ NSError *error;
+ [[NSFileManager defaultManager] createDirectoryAtPath:subDirectoryPath
+ withIntermediateDirectories:YES
+ attributes:nil
+ error:&error];
+ if (error) {
+ FIRMessagingLoggerError(kFIRMessagingMessageCodeMessaging017,
+ @"Cannot create directory %@, error: %@", subDirectoryPath, error);
+ return NO;
+ }
+ } else {
+ if (!hasSubDirectory) {
+ FIRMessagingLoggerError(kFIRMessagingMessageCodeMessaging018,
+ @"Found file instead of directory at %@", subDirectoryPath);
+ return NO;
+ }
+ }
+ return YES;
+}
+
+#pragma mark - Locales
+
++ (NSString *)currentLocale {
+ NSArray *locales = [self firebaseLocales];
+ NSArray *preferredLocalizations =
+ [NSBundle preferredLocalizationsFromArray:locales
+ forPreferences:[NSLocale preferredLanguages]];
+ NSString *legalDocsLanguage = [preferredLocalizations firstObject];
+ // Use en as the default language
+ return legalDocsLanguage ? legalDocsLanguage : @"en";
+}
+
++ (NSArray *)firebaseLocales {
+ NSMutableArray *locales = [NSMutableArray array];
+ NSDictionary *localesMap = [self firebaselocalesMap];
+ for (NSString *key in localesMap) {
+ [locales addObjectsFromArray:localesMap[key]];
+ }
+ return locales;
+}
+
++ (NSDictionary *)firebaselocalesMap {
+ return @{
+ // Albanian
+ @"sq" : @[ @"sq_AL" ],
+ // Belarusian
+ @"be" : @[ @"be_BY" ],
+ // Bulgarian
+ @"bg" : @[ @"bg_BG" ],
+ // Catalan
+ @"ca" : @[ @"ca", @"ca_ES" ],
+ // Croatian
+ @"hr" : @[ @"hr", @"hr_HR" ],
+ // Czech
+ @"cs" : @[ @"cs", @"cs_CZ" ],
+ // Danish
+ @"da" : @[ @"da", @"da_DK" ],
+ // Estonian
+ @"et" : @[ @"et_EE" ],
+ // Finnish
+ @"fi" : @[ @"fi", @"fi_FI" ],
+ // Hebrew
+ @"he" : @[ @"he", @"iw_IL" ],
+ // Hindi
+ @"hi" : @[ @"hi_IN" ],
+ // Hungarian
+ @"hu" : @[ @"hu", @"hu_HU" ],
+ // Icelandic
+ @"is" : @[ @"is_IS" ],
+ // Indonesian
+ @"id" : @[ @"id", @"in_ID", @"id_ID" ],
+ // Irish
+ @"ga" : @[ @"ga_IE" ],
+ // Korean
+ @"ko" : @[ @"ko", @"ko_KR", @"ko-KR" ],
+ // Latvian
+ @"lv" : @[ @"lv_LV" ],
+ // Lithuanian
+ @"lt" : @[ @"lt_LT" ],
+ // Macedonian
+ @"mk" : @[ @"mk_MK" ],
+ // Malay
+ @"ms" : @[ @"ms_MY" ],
+ // Maltese
+ @"ms" : @[ @"mt_MT" ],
+ // Polish
+ @"pl" : @[ @"pl", @"pl_PL", @"pl-PL" ],
+ // Romanian
+ @"ro" : @[ @"ro", @"ro_RO" ],
+ // Russian
+ @"ru" : @[ @"ru_RU", @"ru", @"ru_BY", @"ru_KZ", @"ru-RU" ],
+ // Slovak
+ @"sk" : @[ @"sk", @"sk_SK" ],
+ // Slovenian
+ @"sl" : @[ @"sl_SI" ],
+ // Swedish
+ @"sv" : @[ @"sv", @"sv_SE", @"sv-SE" ],
+ // Turkish
+ @"tr" : @[ @"tr", @"tr-TR", @"tr_TR" ],
+ // Ukrainian
+ @"uk" : @[ @"uk", @"uk_UA" ],
+ // Vietnamese
+ @"vi" : @[ @"vi", @"vi_VN" ],
+ // The following are groups of locales or locales that sub-divide a
+ // language).
+ // Arabic
+ @"ar" : @[
+ @"ar",
+ @"ar_DZ",
+ @"ar_BH",
+ @"ar_EG",
+ @"ar_IQ",
+ @"ar_JO",
+ @"ar_KW",
+ @"ar_LB",
+ @"ar_LY",
+ @"ar_MA",
+ @"ar_OM",
+ @"ar_QA",
+ @"ar_SA",
+ @"ar_SD",
+ @"ar_SY",
+ @"ar_TN",
+ @"ar_AE",
+ @"ar_YE",
+ @"ar_GB",
+ @"ar-IQ",
+ @"ar_US"
+ ],
+ // Simplified Chinese
+ @"zh_Hans" : @[ @"zh_CN", @"zh_SG", @"zh-Hans" ],
+ // Traditional Chinese
+ @"zh_Hant" : @[ @"zh_HK", @"zh_TW", @"zh-Hant", @"zh-HK", @"zh-TW" ],
+ // Dutch
+ @"nl" : @[ @"nl", @"nl_BE", @"nl_NL", @"nl-NL" ],
+ // English
+ @"en" : @[
+ @"en",
+ @"en_AU",
+ @"en_CA",
+ @"en_IN",
+ @"en_IE",
+ @"en_MT",
+ @"en_NZ",
+ @"en_PH",
+ @"en_SG",
+ @"en_ZA",
+ @"en_GB",
+ @"en_US",
+ @"en_AE",
+ @"en-AE",
+ @"en_AS",
+ @"en-AU",
+ @"en_BD",
+ @"en-CA",
+ @"en_EG",
+ @"en_ES",
+ @"en_GB",
+ @"en-GB",
+ @"en_HK",
+ @"en_ID",
+ @"en-IN",
+ @"en_NG",
+ @"en-PH",
+ @"en_PK",
+ @"en-SG",
+ @"en-US"
+ ],
+ // French
+
+ @"fr" : @[
+ @"fr",
+ @"fr_BE",
+ @"fr_CA",
+ @"fr_FR",
+ @"fr_LU",
+ @"fr_CH",
+ @"fr-CA",
+ @"fr-FR",
+ @"fr_MA"
+ ],
+ // German
+ @"de" : @[ @"de", @"de_AT", @"de_DE", @"de_LU", @"de_CH", @"de-DE" ],
+ // Greek
+ @"el" : @[ @"el", @"el_CY", @"el_GR" ],
+ // Italian
+ @"it" : @[ @"it", @"it_IT", @"it_CH", @"it-IT" ],
+ // Japanese
+ @"ja" : @[ @"ja", @"ja_JP", @"ja_JP_JP", @"ja-JP" ],
+ // Norwegian
+ @"no" : @[ @"nb", @"no_NO", @"no_NO_NY", @"nb_NO" ],
+ // Brazilian Portuguese
+ @"pt_BR" : @[ @"pt_BR", @"pt-BR" ],
+ // European Portuguese
+ @"pt_PT" : @[ @"pt", @"pt_PT", @"pt-PT" ],
+ // Serbian
+ @"sr" : @[
+ @"sr_BA",
+ @"sr_ME",
+ @"sr_RS",
+ @"sr_Latn_BA",
+ @"sr_Latn_ME",
+ @"sr_Latn_RS"
+ ],
+ // European Spanish
+ @"es_ES" : @[ @"es", @"es_ES", @"es-ES" ],
+ // Mexican Spanish
+ @"es_MX" : @[ @"es-MX", @"es_MX", @"es_US", @"es-US" ],
+ // Latin American Spanish
+ @"es_419" : @[
+ @"es_AR",
+ @"es_BO",
+ @"es_CL",
+ @"es_CO",
+ @"es_CR",
+ @"es_DO",
+ @"es_EC",
+ @"es_SV",
+ @"es_GT",
+ @"es_HN",
+ @"es_NI",
+ @"es_PA",
+ @"es_PY",
+ @"es_PE",
+ @"es_PR",
+ @"es_UY",
+ @"es_VE",
+ @"es-AR",
+ @"es-CL",
+ @"es-CO"
+ ],
+ // Thai
+ @"th" : @[ @"th", @"th_TH", @"th_TH_TH" ],
+ };
+}
+
+@end
diff --git a/Firebase/Messaging/FIRMessagingCheckinService.h b/Firebase/Messaging/FIRMessagingCheckinService.h
new file mode 100644
index 0000000..155143a
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingCheckinService.h
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+/**
+ * Register the device with Checkin Service and get back the `authID`, `secret token` etc. for the
+ * client. Checkin results are cached in the `FIRMessagingDefaultsManager` and periodically refreshed to
+ * prevent them from being stale. Each client needs to register with checkin before registering
+ * with FIRMessaging.
+ */
+@interface FIRMessagingCheckinService : NSObject
+
+@property(nonatomic, readonly, strong) NSString *deviceAuthID;
+@property(nonatomic, readonly, strong) NSString *secretToken;
+@property(nonatomic, readonly, strong) NSString *versionInfo;
+@property(nonatomic, readonly, assign) BOOL hasValidCheckinInfo;
+
+/**
+ * Verify if valid checkin preferences have been loaded in memory.
+ *
+ * @return YES if valid checkin preferences exist in memory else NO.
+ */
+- (BOOL)hasValidCheckinInfo;
+
+/**
+ * Try to load prefetched checkin preferences from the cache. This supports the use case where
+ * InstanceID library has already obtained a valid checkin and we should be using that.
+ *
+ * This should be used as a last gasp effort to retreive any cached checkin preferences before
+ * hitting the FIRMessaging backend to retrieve new preferences.
+ *
+ * Note this is only required because InstanceID and FIRMessaging both require checkin preferences which
+ * need to be synced with each other.
+ *
+ * @return YES if successfully loaded cached checkin preferences into memory else NO.
+ */
+- (BOOL)tryToLoadPrefetchedCheckinPreferences;
+
+@end
diff --git a/Firebase/Messaging/FIRMessagingCheckinService.m b/Firebase/Messaging/FIRMessagingCheckinService.m
new file mode 100644
index 0000000..9dad847
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingCheckinService.m
@@ -0,0 +1,132 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRMessagingCheckinService.h"
+
+#import "FIRMessagingUtilities.h"
+#import "NSError+FIRMessaging.h"
+
+@interface FIRMessagingCheckinService ()
+
+// This property is of type FIRInstanceIDCheckinPreferences, if InstanceID was directly linkable
+@property(nonatomic, readwrite, strong) id checkinPreferences;
+
+@end
+
+@implementation FIRMessagingCheckinService;
+
+#pragma mark - Reflection-Based Getter Functions
+
+// Encapsulates the -hasValidCheckinInfo method of FIRInstanceIDCheckinPreferences
+BOOL FIRMessagingCheckinService_hasValidCheckinInfo(id checkinPreferences) {
+ SEL hasValidCheckinInfoSelector = NSSelectorFromString(@"hasValidCheckinInfo");
+ if (![checkinPreferences respondsToSelector:hasValidCheckinInfoSelector]) {
+ // Can't check hasValidCheckinInfo
+ return NO;
+ }
+
+ // Since hasValidCheckinInfo returns a BOOL, use NSInvocation
+ NSMethodSignature *methodSignature =
+ [[checkinPreferences class] instanceMethodSignatureForSelector:hasValidCheckinInfoSelector];
+ NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature];
+ invocation.selector = hasValidCheckinInfoSelector;
+ invocation.target = checkinPreferences;
+ [invocation invoke];
+ BOOL returnValue;
+ [invocation getReturnValue:&returnValue];
+ return returnValue;
+}
+
+// Returns a non-scalar (id) object based on the property name
+id FIRMessagingCheckinService_propertyNamed(id checkinPreferences, NSString *propertyName) {
+ SEL propertyGetterSelector = NSSelectorFromString(propertyName);
+ if ([checkinPreferences respondsToSelector:propertyGetterSelector]) {
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
+ return [checkinPreferences performSelector:propertyGetterSelector];
+#pragma clang diagnostic pop
+ }
+ return nil;
+}
+
+#pragma mark - Methods
+
+- (BOOL)tryToLoadPrefetchedCheckinPreferences {
+ Class instanceIDClass = NSClassFromString(@"FIRInstanceID");
+ if (!instanceIDClass) {
+ // InstanceID is not linked
+ return NO;
+ }
+
+ // [FIRInstanceID instanceID]
+ SEL instanceIDSelector = NSSelectorFromString(@"instanceID");
+ if (![instanceIDClass respondsToSelector:instanceIDSelector]) {
+ return NO;
+ }
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
+ id instanceID = [instanceIDClass performSelector:instanceIDSelector];
+#pragma clang diagnostic pop
+ if (!instanceID) {
+ // Instance ID singleton not available
+ return NO;
+ }
+
+ // [[FIRInstanceID instanceID] cachedCheckinPreferences]
+ SEL cachedCheckinPrefsSelector = NSSelectorFromString(@"cachedCheckinPreferences");
+ if (![instanceID respondsToSelector:cachedCheckinPrefsSelector]) {
+ // cachedCheckinPreferences is not accessible
+ return NO;
+ }
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
+ id checkinPreferences = [instanceID performSelector:cachedCheckinPrefsSelector];
+#pragma clang diagnostic pop
+ if (!checkinPreferences) {
+ // No cached checkin prefs
+ return NO;
+ }
+
+ BOOL hasValidInfo = FIRMessagingCheckinService_hasValidCheckinInfo(checkinPreferences);
+ if (hasValidInfo) {
+ self.checkinPreferences = checkinPreferences;
+ }
+ return hasValidInfo;
+}
+
+#pragma mark - API
+
+- (NSString *)deviceAuthID {
+ return FIRMessagingCheckinService_propertyNamed(self.checkinPreferences, @"deviceID");
+}
+
+- (NSString *)secretToken {
+ return FIRMessagingCheckinService_propertyNamed(self.checkinPreferences, @"secretToken");
+}
+
+- (NSString *)versionInfo {
+ return FIRMessagingCheckinService_propertyNamed(self.checkinPreferences, @"versionInfo");
+}
+
+- (NSString *)digest {
+ return FIRMessagingCheckinService_propertyNamed(self.checkinPreferences, @"digest");
+}
+
+- (BOOL)hasValidCheckinInfo {
+ return FIRMessagingCheckinService_hasValidCheckinInfo(self.checkinPreferences);
+}
+
+@end
diff --git a/Firebase/Messaging/FIRMessagingClient.h b/Firebase/Messaging/FIRMessagingClient.h
new file mode 100644
index 0000000..1726428
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingClient.h
@@ -0,0 +1,156 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRMessagingTopicsCommon.h"
+
+@class FIRReachabilityChecker;
+@class GPBMessage;
+
+@class FIRMessagingConnection;
+@class FIRMessagingDataMessageManager;
+@class FIRMessagingRmqManager;
+
+/**
+ * Callback to handle MCS connection requests.
+ *
+ * @param error The error object if any while trying to connect with MCS else nil.
+ */
+typedef void(^FIRMessagingConnectCompletionHandler)(NSError *error);
+
+@protocol FIRMessagingClientDelegate <NSObject>
+
+@end
+
+/**
+ * The client handles the subscribe/unsubscribe for an unregistered senderID
+ * and device. It also manages the FIRMessaging data connection, the exponential backoff
+ * algorithm in case of registration failures, sign in failures and unregister
+ * failures. It also handles the reconnect logic if the FIRMessaging connection is
+ * broken off by some error during an active session.
+ */
+@interface FIRMessagingClient : NSObject
+
+@property(nonatomic, readonly, strong) FIRMessagingConnection *connection;
+@property(nonatomic, readwrite, weak) FIRMessagingDataMessageManager *dataMessageManager;
+
+// Designated initializer
+- (instancetype)initWithDelegate:(id<FIRMessagingClientDelegate>)delegate
+ reachability:(FIRReachabilityChecker *)reachability
+ rmq2Manager:(FIRMessagingRmqManager *)rmq2Manager;
+
+- (void)teardown;
+
+- (void)cancelAllRequests;
+
+#pragma mark - FIRMessaging subscribe
+
+/**
+ * Update the subscription associated with the given token and topic.
+ *
+ * For a to-be-created subscription we check if the client is already
+ * subscribed to the topic or not. If subscribed we should have the
+ * subscriptionID in the cache and we return from there itself, else we call
+ * the FIRMessaging backend to create a new subscription for the topic for this client.
+ *
+ * For delete subscription requests we delete the stored subscription in the
+ * client and then invoke the FIRMessaging backend to delete the existing subscription
+ * completely.
+ *
+ * @param token The token associated with the device.
+ * @param topic The topic for which the subscription should be updated.
+ * @param options The options to be passed in to the subscription request.
+ * @param shouldDelete If YES this would delete the subscription from the cache
+ * and also let the FIRMessaging backend know that we need to delete
+ * the subscriptionID associated with this topic.
+ * If NO we try to create a new subscription for the given
+ * token and topic.
+ * @param handler The handler to invoke once the subscription request
+ * finishes.
+ */
+- (void)updateSubscriptionWithToken:(NSString *)token
+ topic:(NSString *)topic
+ options:(NSDictionary *)options
+ shouldDelete:(BOOL)shouldDelete
+ handler:(FIRMessagingTopicOperationCompletion)handler;
+
+#pragma mark - MCS Connection
+
+/**
+ * Create a MCS connection.
+ *
+ * @param handler The handler to be invokend once the connection is setup. If
+ * setting up the connection fails we invoke the handler with
+ * an appropriate error object.
+ */
+- (void)connectWithHandler:(FIRMessagingConnectCompletionHandler)handler;
+
+/**
+ * Disconnect the current MCS connection. If there is no valid connection this
+ * should be a NO-OP.
+ */
+- (void)disconnect;
+
+#pragma mark - MCS Connection State
+
+/**
+ * If we are connected to MCS or not. This doesn't take into account the fact if
+ * the client has been signed in(verified) by MCS.
+ *
+ * @return YES if we are signed in or connecting and trying to sign-in else NO.
+ */
+@property(nonatomic, readonly) BOOL isConnected;
+
+/**
+ * If we have an active MCS connection
+ *
+ * @return YES if we have an active MCS connection else NO.
+ */
+@property(nonatomic, readonly) BOOL isConnectionActive;
+
+/**
+ * If we should be connected to MCS
+ *
+ * @return YES if we have attempted a connection and not requested to disconect.
+ */
+@property(nonatomic, readonly) BOOL shouldStayConnected;
+
+/**
+ * Schedule a retry to connect to MCS. If `immediately` is `YES` try to
+ * schedule a retry now else retry with some delay.
+ *
+ * @param immediately Should retry right now.
+ */
+- (void)retryConnectionImmediately:(BOOL)immediately;
+
+#pragma mark - Messages
+
+/**
+ * Send a message over the MCS connection.
+ *
+ * @param message Message to be sent.
+ */
+- (void)sendMessage:(GPBMessage *)message;
+
+/**
+ * Send message if we have an active MCS connection. If not cache the message
+ * for this session and in case we are able to re-establish the connection try
+ * again else drop it. This should only be used for TTL=0 messages for now.
+ *
+ * @param message Message to be sent.
+ */
+- (void)sendOnConnectOrDrop:(GPBMessage *)message;
+
+@end
diff --git a/Firebase/Messaging/FIRMessagingClient.m b/Firebase/Messaging/FIRMessagingClient.m
new file mode 100644
index 0000000..c01aecc
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingClient.m
@@ -0,0 +1,490 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRMessagingClient.h"
+
+#import "FIRMessagingConnection.h"
+#import "FIRMessagingDataMessageManager.h"
+#import "FIRMessagingDefines.h"
+#import "FIRMessagingLogger.h"
+#import "FIRMessagingRegistrar.h"
+#import "FIRMessagingRmqManager.h"
+#import "FIRMessagingTopicsCommon.h"
+#import "FIRMessagingUtilities.h"
+#import "FIRReachabilityChecker.h"
+#import "NSError+FIRMessaging.h"
+
+static const NSTimeInterval kConnectTimeoutInterval = 40.0;
+static const NSTimeInterval kReconnectDelayInSeconds = 2 * 60; // 2 minutes
+
+static const NSUInteger kMaxRetryExponent = 10; // 2^10 = 1024 seconds ~= 17 minutes
+
+static NSString *const kFIRMessagingMCSServerHost = @"mtalk.google.com";
+static NSUInteger const kFIRMessagingMCSServerPort = 5228;
+
+// register device with checkin
+typedef void(^FIRMessagingRegisterDeviceHandler)(NSError *error);
+
+static NSString *FIRMessagingServerHost() {
+ static NSString *serverHost = nil;
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ NSDictionary *environment = [[NSProcessInfo processInfo] environment];
+ NSString *customServerHostAndPort = environment[@"FCM_MCS_HOST"];
+ NSString *host = [customServerHostAndPort componentsSeparatedByString:@":"].firstObject;
+ if (host) {
+ serverHost = host;
+ } else {
+ serverHost = kFIRMessagingMCSServerHost;
+ }
+ });
+ return serverHost;
+}
+
+static NSUInteger FIRMessagingServerPort() {
+ static NSUInteger serverPort = kFIRMessagingMCSServerPort;
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ NSDictionary *environment = [[NSProcessInfo processInfo] environment];
+ NSString *customServerHostAndPort = environment[@"FCM_MCS_HOST"];
+ NSArray<NSString *> *components = [customServerHostAndPort componentsSeparatedByString:@":"];
+ NSUInteger port = (NSUInteger)[components.lastObject integerValue];
+ if (port != 0) {
+ serverPort = port;
+ }
+ });
+ return serverPort;
+}
+
+@interface FIRMessagingClient () <FIRMessagingConnectionDelegate>
+
+@property(nonatomic, readwrite, weak) id<FIRMessagingClientDelegate> clientDelegate;
+@property(nonatomic, readwrite, strong) FIRMessagingConnection *connection;
+@property(nonatomic, readwrite, strong) FIRMessagingRegistrar *registrar;
+
+@property(nonatomic, readwrite, strong) NSString *senderId;
+
+// FIRMessagingService owns these instances
+@property(nonatomic, readwrite, weak) FIRMessagingRmqManager *rmq2Manager;
+@property(nonatomic, readwrite, weak) FIRReachabilityChecker *reachability;
+
+@property(nonatomic, readwrite, assign) int64_t lastConnectedTimestamp;
+@property(nonatomic, readwrite, assign) int64_t lastDisconnectedTimestamp;
+@property(nonatomic, readwrite, assign) NSUInteger connectRetryCount;
+
+// Should we stay connected to MCS or not. Should be YES throughout the lifetime
+// of a MCS connection. If set to NO it signifies that an existing MCS connection
+// should be disconnected.
+@property(nonatomic, readwrite, assign) BOOL stayConnected;
+@property(nonatomic, readwrite, assign) NSTimeInterval connectionTimeoutInterval;
+
+// Used if the MCS connection suddenly breaksdown in the middle and we want to reconnect
+// with some permissible delay we schedule a reconnect and set it to YES and when it's
+// scheduled this will be set back to NO.
+@property(nonatomic, readwrite, assign) BOOL didScheduleReconnect;
+
+// handlers
+@property(nonatomic, readwrite, copy) FIRMessagingConnectCompletionHandler connectHandler;
+
+@end
+
+@implementation FIRMessagingClient
+
+- (instancetype)init {
+ FIRMessagingInvalidateInitializer();
+}
+
+- (instancetype)initWithDelegate:(id<FIRMessagingClientDelegate>)delegate
+ reachability:(FIRReachabilityChecker *)reachability
+ rmq2Manager:(FIRMessagingRmqManager *)rmq2Manager {
+ self = [super init];
+ if (self) {
+ _reachability = reachability;
+ _clientDelegate = delegate;
+ _rmq2Manager = rmq2Manager;
+ _registrar = [[FIRMessagingRegistrar alloc] init];
+ _connectionTimeoutInterval = kConnectTimeoutInterval;
+ }
+ return self;
+}
+
+- (void)teardown {
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeClient000, @"");
+ self.stayConnected = NO;
+
+ // Clear all the handlers
+ self.connectHandler = nil;
+
+ [self.connection teardown];
+
+ // Stop all subscription requests
+ [self.registrar cancelAllRequests];
+
+ _FIRMessagingDevAssert(self.connection.state == kFIRMessagingConnectionNotConnected, @"Did not disconnect");
+ [NSObject cancelPreviousPerformRequestsWithTarget:self];
+}
+
+- (void)cancelAllRequests {
+ // Stop any checkin requests or any subscription requests
+ [self.registrar cancelAllRequests];
+
+ // Stop any future connection requests to MCS
+ if (self.stayConnected && self.isConnected && !self.isConnectionActive) {
+ self.stayConnected = NO;
+ [NSObject cancelPreviousPerformRequestsWithTarget:self];
+ }
+}
+
+#pragma mark - FIRMessaging subscribe
+
+- (void)updateSubscriptionWithToken:(NSString *)token
+ topic:(NSString *)topic
+ options:(NSDictionary *)options
+ shouldDelete:(BOOL)shouldDelete
+ handler:(FIRMessagingTopicOperationCompletion)handler {
+
+ _FIRMessagingDevAssert(handler != nil, @"Invalid handler to FIRMessaging subscribe");
+
+ FIRMessagingTopicOperationCompletion completion =
+ ^void(FIRMessagingTopicOperationResult result, NSError * error) {
+ if (error) {
+ FIRMessagingLoggerError(kFIRMessagingMessageCodeClient001, @"Failed to subscribe to topic %@",
+ error);
+ } else {
+ if (shouldDelete) {
+ FIRMessagingLoggerInfo(kFIRMessagingMessageCodeClient002,
+ @"Successfully unsubscribed from topic %@", topic);
+ } else {
+ FIRMessagingLoggerInfo(kFIRMessagingMessageCodeClient003,
+ @"Successfully subscribed to topic %@", topic);
+ }
+ }
+ handler(result, error);
+ };
+
+ [self.registrar tryToLoadValidCheckinInfo];
+ [self.registrar updateSubscriptionToTopic:topic
+ withToken:token
+ options:options
+ shouldDelete:shouldDelete
+ handler:completion];
+}
+
+#pragma mark - MCS Connection
+
+- (BOOL)isConnected {
+ return self.stayConnected && self.connection.state != kFIRMessagingConnectionNotConnected;
+}
+
+- (BOOL)isConnectionActive {
+ return self.stayConnected && self.connection.state == kFIRMessagingConnectionSignedIn;
+}
+
+- (BOOL)shouldStayConnected {
+ return self.stayConnected;
+}
+
+- (void)retryConnectionImmediately:(BOOL)immediately {
+ // Do not connect to an invalid host or an invalid port
+ if (!self.stayConnected || !self.connection.host || self.connection.port == 0) {
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeClient004,
+ @"FIRMessaging connection will not reconnect to MCS. "
+ @"Stay connected: %d",
+ self.stayConnected);
+ return;
+ }
+ if (self.isConnectionActive) {
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeClient005,
+ @"FIRMessaging Connection skip retry, active");
+ // already connected and logged in.
+ // Heartbeat alarm is set and will force close the connection
+ return;
+ }
+ if (self.isConnected) {
+ // already connected and logged in.
+ // Heartbeat alarm is set and will force close the connection
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeClient006,
+ @"FIRMessaging Connection skip retry, connected");
+ return;
+ }
+
+ if (immediately) {
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeClient007,
+ @"Try to connect to MCS immediately");
+ [self tryToConnect];
+ } else {
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeClient008, @"Try to connect to MCS lazily");
+ // Avoid all the other logic that we have in other clients, since this would always happen
+ // when the app is in the foreground and since the FIRMessaging connection isn't shared with any other
+ // app we can be more aggressive in reconnections
+ if (!self.didScheduleReconnect) {
+ FIRMessaging_WEAKIFY(self);
+ dispatch_after(dispatch_time(DISPATCH_TIME_NOW,
+ (int64_t)(kReconnectDelayInSeconds * NSEC_PER_SEC)),
+ dispatch_get_main_queue(), ^{
+ FIRMessaging_STRONGIFY(self);
+ self.didScheduleReconnect = NO;
+ [self tryToConnect];
+ });
+
+ self.didScheduleReconnect = YES;
+ }
+ }
+}
+
+- (void)connectWithHandler:(FIRMessagingConnectCompletionHandler)handler {
+ if (self.isConnected) {
+ NSError *error = [NSError fcm_errorWithCode:kFIRMessagingErrorCodeAlreadyConnected
+ userInfo:@{
+ NSLocalizedFailureReasonErrorKey: @"FIRMessaging is already connected",
+ }];
+ handler(error);
+ return;
+ }
+ self.lastDisconnectedTimestamp = FIRMessagingCurrentTimestampInMilliseconds();
+ self.connectHandler = handler;
+ [self.registrar tryToLoadValidCheckinInfo];
+ [self connect];
+}
+
+- (void)connect {
+ // reset retry counts
+ self.connectRetryCount = 0;
+
+ if (self.isConnected) {
+ return;
+ }
+
+ self.stayConnected = YES;
+ BOOL isRegistrationComplete = [self.registrar hasValidCheckinInfo];
+
+ if (!isRegistrationComplete) {
+ if (![self.registrar tryToLoadValidCheckinInfo]) {
+ if (self.connectHandler) {
+ NSError *error = [NSError errorWithFCMErrorCode:kFIRMessagingErrorCodeMissingDeviceID];
+ self.connectHandler(error);
+ }
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeClient009,
+ @"Failed to connect to MCS. No deviceID and secret found.");
+ return;
+ }
+ }
+ [self setupConnectionAndConnect];
+}
+
+- (void)disconnect {
+ // user called disconnect
+ // We don't want to connect later even if no network is available.
+ [self disconnectWithTryToConnectLater:NO];
+}
+
+/**
+ * Disconnect the current client connection. Also explicitly stop and connction retries.
+ *
+ * @param tryToConnectLater If YES will try to connect later when sending upstream messages
+ * else if NO do not connect again until user explicitly calls
+ * connect.
+ */
+- (void)disconnectWithTryToConnectLater:(BOOL)tryToConnectLater {
+
+ self.stayConnected = tryToConnectLater;
+ [self.connection signOut];
+ _FIRMessagingDevAssert(self.connection.state == kFIRMessagingConnectionNotConnected,
+ @"FIRMessaging connection did not disconnect");
+
+ // since we can disconnect while still trying to establish the connection it's required to
+ // cancel all performSelectors else the object might be retained
+ [NSObject cancelPreviousPerformRequestsWithTarget:self
+ selector:@selector(tryToConnect)
+ object:nil];
+ [NSObject cancelPreviousPerformRequestsWithTarget:self
+ selector:@selector(didConnectTimeout)
+ object:nil];
+ self.connectHandler = nil;
+}
+
+
+#pragma mark - Messages
+
+- (void)sendMessage:(GPBMessage *)message {
+ [self.connection sendProto:message];
+}
+
+- (void)sendOnConnectOrDrop:(GPBMessage *)message {
+ [self.connection sendOnConnectOrDrop:message];
+}
+
+#pragma mark - FIRMessagingConnectionDelegate
+
+- (void)connection:(FIRMessagingConnection *)fcmConnection
+ didCloseForReason:(FIRMessagingConnectionCloseReason)reason {
+
+ self.lastDisconnectedTimestamp = FIRMessagingCurrentTimestampInMilliseconds();
+
+ if (reason == kFIRMessagingConnectionCloseReasonSocketDisconnected) {
+ // Cancel the not-yet-triggered timeout task before rescheduling, in case the previous sign in
+ // failed, due to a connection error caused by bad network.
+ [NSObject cancelPreviousPerformRequestsWithTarget:self
+ selector:@selector(didConnectTimeout)
+ object:nil];
+ }
+ if (self.stayConnected) {
+ [self scheduleConnectRetry];
+ }
+}
+
+- (void)didLoginWithConnection:(FIRMessagingConnection *)fcmConnection {
+ // Cancel the not-yet-triggered timeout task.
+ [NSObject cancelPreviousPerformRequestsWithTarget:self
+ selector:@selector(didConnectTimeout)
+ object:nil];
+ self.connectRetryCount = 0;
+ self.lastConnectedTimestamp = FIRMessagingCurrentTimestampInMilliseconds();
+
+
+ [self.dataMessageManager setDeviceAuthID:self.registrar.deviceAuthID
+ secretToken:self.registrar.secretToken];
+ if (self.connectHandler) {
+ self.connectHandler(nil);
+ // notified the third party app with the registrationId.
+ // we don't want them to know about the connection status and how it changes
+ // so remove this handler
+ self.connectHandler = nil;
+ }
+}
+
+- (void)connectionDidRecieveMessage:(GtalkDataMessageStanza *)message {
+ NSDictionary *parsedMessage = [self.dataMessageManager processPacket:message];
+ if ([parsedMessage count]) {
+ [self.dataMessageManager didReceiveParsedMessage:parsedMessage];
+ }
+}
+
+- (int)connectionDidReceiveAckForRmqIds:(NSArray *)rmqIds {
+ NSSet *rmqIDSet = [NSSet setWithArray:rmqIds];
+ NSMutableArray *messagesSent = [NSMutableArray arrayWithCapacity:rmqIds.count];
+ [self.rmq2Manager scanWithRmqMessageHandler:nil
+ dataMessageHandler:^(int64_t rmqId, GtalkDataMessageStanza *stanza) {
+ NSString *rmqIdString = [NSString stringWithFormat:@"%lld", rmqId];
+ if ([rmqIDSet containsObject:rmqIdString]) {
+ [messagesSent addObject:stanza];
+ }
+ }];
+ for (GtalkDataMessageStanza *message in messagesSent) {
+ [self.dataMessageManager didSendDataMessageStanza:message];
+ }
+ return [self.rmq2Manager removeRmqMessagesWithRmqIds:rmqIds];
+}
+
+#pragma mark - Private
+
+- (void)setupConnectionAndConnect {
+ [self setupConnection];
+ [self tryToConnect];
+}
+
+- (void)setupConnection {
+ NSString *host = FIRMessagingServerHost();
+ NSUInteger port = FIRMessagingServerPort();
+ _FIRMessagingDevAssert([host length] > 0 && port != 0, @"Invalid port or host");
+
+ if (self.connection != nil) {
+ // if there is an old connection, explicitly sign it off.
+ [self.connection signOut];
+ self.connection.delegate = nil;
+ }
+ self.connection = [[FIRMessagingConnection alloc] initWithAuthID:self.registrar.deviceAuthID
+ token:self.registrar.secretToken
+ host:host
+ port:port
+ runLoop:[NSRunLoop mainRunLoop]
+ rmq2Manager:self.rmq2Manager
+ fcmManager:self.dataMessageManager];
+ self.connection.delegate = self;
+}
+
+- (void)tryToConnect {
+ if (!self.stayConnected) {
+ return;
+ }
+
+ // Cancel any other pending signin requests.
+ [NSObject cancelPreviousPerformRequestsWithTarget:self
+ selector:@selector(tryToConnect)
+ object:nil];
+
+ // Do not re-sign in if there is already a connection in progress.
+ if (self.connection.state != kFIRMessagingConnectionNotConnected) {
+ return;
+ }
+
+ _FIRMessagingDevAssert(self.registrar.deviceAuthID.length > 0 &&
+ self.registrar.secretToken.length > 0 &&
+ self.connection != nil,
+ @"Invalid state cannot connect");
+
+ self.connectRetryCount = MIN(kMaxRetryExponent, self.connectRetryCount + 1);
+ [self performSelector:@selector(didConnectTimeout)
+ withObject:nil
+ afterDelay:self.connectionTimeoutInterval];
+ [self.connection signIn];
+}
+
+- (void)didConnectTimeout {
+ _FIRMessagingDevAssert(self.connection.state != kFIRMessagingConnectionSignedIn,
+ @"Invalid state for MCS connection");
+
+ if (self.stayConnected) {
+ [self.connection signOut];
+ [self scheduleConnectRetry];
+ }
+}
+
+#pragma mark - Schedulers
+
+- (void)scheduleConnectRetry {
+ FIRReachabilityStatus status = self.reachability.reachabilityStatus;
+ BOOL isReachable = (status == kFIRReachabilityViaWifi || status == kFIRReachabilityViaCellular);
+ if (!isReachable) {
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeClient010,
+ @"Internet not reachable when signing into MCS during a retry");
+
+ FIRMessagingConnectCompletionHandler handler = [self.connectHandler copy];
+ // disconnect before issuing a callback
+ [self disconnectWithTryToConnectLater:YES];
+ NSError *error = [NSError errorWithDomain:@"No internet available, cannot connect to FIRMessaging"
+ code:kFIRMessagingErrorCodeNetwork
+ userInfo:nil];
+ if (handler) {
+ handler(error);
+ self.connectHandler = nil;
+ }
+ return;
+ }
+
+ NSUInteger retryInterval = [self nextRetryInterval];
+
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeClient011,
+ @"Failed to sign in to MCS, retry in %lu seconds",
+ _FIRMessaging_UL(retryInterval));
+ [self performSelector:@selector(tryToConnect) withObject:nil afterDelay:retryInterval];
+}
+
+- (NSUInteger)nextRetryInterval {
+ return 1u << self.connectRetryCount;
+}
+
+@end
diff --git a/Firebase/Messaging/FIRMessagingCodedInputStream.h b/Firebase/Messaging/FIRMessagingCodedInputStream.h
new file mode 100644
index 0000000..8f22290
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingCodedInputStream.h
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+@interface FIRMessagingCodedInputStream : NSObject
+
+@property(nonatomic, readonly, assign) size_t offset;
+
+- (instancetype)initWithData:(NSData *)data;
+- (BOOL)readTag:(int8_t *)tag;
+- (BOOL)readLength:(int32_t *)length;
+- (NSData *)readDataWithLength:(uint32_t)length;
+
+@end
diff --git a/Firebase/Messaging/FIRMessagingCodedInputStream.m b/Firebase/Messaging/FIRMessagingCodedInputStream.m
new file mode 100644
index 0000000..82c0677
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingCodedInputStream.m
@@ -0,0 +1,142 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRMessagingCodedInputStream.h"
+#import "FIRMessagingDefines.h"
+
+typedef struct {
+ const void *bytes;
+ size_t bufferSize;
+ size_t bufferPos;
+} BufferState;
+
+static BOOL CheckSize(BufferState *state, size_t size) {
+ size_t newSize = state->bufferPos + size;
+ if (newSize > state->bufferSize) {
+ return NO;
+ }
+ return YES;
+}
+
+static BOOL ReadRawByte(BufferState *state, int8_t *output) {
+ _FIRMessagingDevAssert(output != NULL && state != NULL, @"Invalid parameters");
+
+ if (CheckSize(state, sizeof(int8_t))) {
+ *output = ((int8_t *)state->bytes)[state->bufferPos++];
+ return YES;
+ }
+ return NO;
+}
+
+static BOOL ReadRawVarInt32(BufferState *state, int32_t *output) {
+ _FIRMessagingDevAssert(output != NULL && state != NULL, @"Invalid parameters");
+
+ int8_t tmp = 0;
+ if (!ReadRawByte(state, &tmp)) {
+ return NO;
+ }
+ if (tmp >= 0) {
+ *output = tmp;
+ return YES;
+ }
+ int32_t result = tmp & 0x7f;
+ if (!ReadRawByte(state, &tmp)) {
+ return NO;
+ }
+ if (tmp >= 0) {
+ result |= tmp << 7;
+ } else {
+ result |= (tmp & 0x7f) << 7;
+ if (!ReadRawByte(state, &tmp)) {
+ return NO;
+ }
+ if (tmp >= 0) {
+ result |= tmp << 14;
+ } else {
+ result |= (tmp & 0x7f) << 14;
+ if (!ReadRawByte(state, &tmp)) {
+ return NO;
+ }
+ if (tmp >= 0) {
+ result |= tmp << 21;
+ } else {
+ result |= (tmp & 0x7f) << 21;
+ if (!ReadRawByte(state, &tmp)) {
+ return NO;
+ }
+ result |= tmp << 28;
+ if (tmp < 0) {
+ // Discard upper 32 bits.
+ for (int i = 0; i < 5; ++i) {
+ if (!ReadRawByte(state, &tmp)) {
+ return NO;
+ }
+ if (tmp >= 0) {
+ *output = result;
+ return YES;
+ }
+ }
+ return NO;
+ }
+ }
+ }
+ }
+ *output = result;
+ return YES;
+}
+
+@interface FIRMessagingCodedInputStream()
+
+@property(nonatomic, readwrite, strong) NSData *buffer;
+@property(nonatomic, readwrite, assign) BufferState state;
+
+@end
+
+@implementation FIRMessagingCodedInputStream;
+
+- (instancetype)initWithData:(NSData *)data {
+ self = [super init];
+ if (self) {
+ _buffer = data;
+ _state.bytes = _buffer.bytes;
+ _state.bufferSize = _buffer.length;
+ }
+ return self;
+}
+
+- (size_t)offset {
+ return _state.bufferPos;
+}
+
+- (BOOL)readTag:(int8_t *)tag {
+ return ReadRawByte(&_state, tag);
+}
+
+- (BOOL)readLength:(int32_t *)length {
+ return ReadRawVarInt32(&_state, length);
+}
+
+- (NSData *)readDataWithLength:(uint32_t)length {
+ if (!CheckSize(&_state, length)) {
+ return nil;
+ }
+ const void *bytesToRead = _state.bytes + _state.bufferPos;
+ NSData *result = [NSData dataWithBytes:bytesToRead length:length];
+ _state.bufferPos += length;
+ return result;
+}
+
+@end
diff --git a/Firebase/Messaging/FIRMessagingConfig.h b/Firebase/Messaging/FIRMessagingConfig.h
new file mode 100644
index 0000000..09a9ec7
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingConfig.h
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+typedef NS_ENUM(int8_t, FIRMessagingLogLevel) {
+ kFIRMessagingLogLevelDebug,
+ kFIRMessagingLogLevelInfo,
+ kFIRMessagingLogLevelError,
+ kFIRMessagingLogLevelAssert,
+};
+
+/**
+ * Config used to set different options in Firebase Messaging.
+ */
+@interface FIRMessagingConfig : NSObject
+
+/**
+ * The log level for the FIRMessaging library. Valid values are `kFIRMessagingLogLevelDebug`,
+ * `kFIRMessagingLogLevelInfo`, `kFIRMessagingLogLevelError`, and `kFIRMessagingLogLevelAssert`.
+ */
+@property(nonatomic, readwrite, assign) FIRMessagingLogLevel logLevel;
+
+/**
+ * Get default configuration for FIRMessaging. The default config has logLevel set to
+ * `kFIRMessagingLogLevelError` and `receiverDelegate` is set to nil.
+ *
+ * @return FIRMessagingConfig sharedInstance.
+ */
++ (instancetype)defaultConfig;
+
+@end
+
diff --git a/Firebase/Messaging/FIRMessagingConfig.m b/Firebase/Messaging/FIRMessagingConfig.m
new file mode 100644
index 0000000..e7674c3
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingConfig.m
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRMessagingConfig.h"
+#import "FIRMessagingDefines.h"
+
+@implementation FIRMessagingConfig
+
+- (instancetype)init {
+ FIRMessagingInvalidateInitializer();
+}
+
++ (instancetype)defaultConfig {
+ return [[FIRMessagingConfig alloc] initWithDefaultConfig];
+}
+
+- (instancetype)initWithDefaultConfig {
+ self = [super init];
+ if (self) {
+ self.logLevel = kFIRMessagingLogLevelError;
+ }
+ return self;
+}
+
+@end
diff --git a/Firebase/Messaging/FIRMessagingConnection.h b/Firebase/Messaging/FIRMessagingConnection.h
new file mode 100644
index 0000000..e78adbf
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingConnection.h
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+@class FIRMessagingConnection;
+@class FIRMessagingDataMessageManager;
+@class FIRMessagingRmqManager;
+
+@class GtalkDataMessageStanza;
+@class GPBMessage;
+
+typedef void (^FIRMessagingMessageHandler)(NSDictionary *);
+
+typedef NS_ENUM(NSUInteger, FIRMessagingConnectionState) {
+ kFIRMessagingConnectionNotConnected = 0,
+ kFIRMessagingConnectionConnecting,
+ kFIRMessagingConnectionConnected,
+ kFIRMessagingConnectionSignedIn,
+};
+
+typedef NS_ENUM(NSUInteger, FIRMessagingConnectionCloseReason) {
+ kFIRMessagingConnectionCloseReasonSocketDisconnected = 0,
+ kFIRMessagingConnectionCloseReasonTimeout,
+ kFIRMessagingConnectionCloseReasonUserDisconnect,
+};
+
+@protocol FIRMessagingConnectionDelegate<NSObject>
+
+- (void)connection:(FIRMessagingConnection *)fcmConnection
+ didCloseForReason:(FIRMessagingConnectionCloseReason)reason;
+- (void)didLoginWithConnection:(FIRMessagingConnection *)fcmConnection;
+- (void)connectionDidRecieveMessage:(GtalkDataMessageStanza *)message;
+/**
+ * Called when a stream ACK or a selective ACK are received - this indicates the
+ * message has been received by MCS.
+ * @return The count of rmqIds deleted from the client RMQ store.
+ */
+- (int)connectionDidReceiveAckForRmqIds:(NSArray *)rmqIds;
+
+@end
+
+
+/**
+ * This class maintains the actual FIRMessaging connection that we use to receive and send messages
+ * while the app is in foreground. Once we have a registrationID from the FIRMessaging backend we
+ * are able to set up this connection which is used for any further communication with FIRMessaging
+ * backend. In case the connection breaks off while the app is still being used we try to rebuild
+ * the connection with an exponential backoff.
+ *
+ * This class also notifies the delegate about the main events happening in the lifcycle of the
+ * FIRMessaging connection (read FIRMessagingConnectionDelegate). All of the `on-the-wire`
+ * interactions with FIRMessaging are channelled through here.
+ */
+@interface FIRMessagingConnection : NSObject
+
+@property(nonatomic, readwrite, assign) int64_t lastHeartbeatPingTimestamp;
+@property(nonatomic, readonly, assign) FIRMessagingConnectionState state;
+@property(nonatomic, readonly, copy) NSString *host;
+@property(nonatomic, readonly, assign) NSUInteger port;
+@property(nonatomic, readwrite, weak) id<FIRMessagingConnectionDelegate> delegate;
+
+- (instancetype)initWithAuthID:(NSString *)authId
+ token:(NSString *)token
+ host:(NSString *)host
+ port:(NSUInteger)port
+ runLoop:(NSRunLoop *)runLoop
+ rmq2Manager:(FIRMessagingRmqManager *)rmq2Manager
+ fcmManager:(FIRMessagingDataMessageManager *)dataMessageManager;
+
+- (void)signIn; // connect
+- (void)signOut; // disconnect
+
+/**
+ * Teardown the FIRMessaging connection and deallocate the resources being held up by the
+ * connection.
+ */
+- (void)teardown;
+
+/**
+ * Send proto to the wire. The message will be cached before we try to send so that in case of
+ * failure we can send it again later on when we have connection.
+ */
+- (void)sendProto:(GPBMessage *)proto;
+
+/**
+ * Send a message after the currently in progress connection succeeds, otherwise drop it.
+ *
+ * This should be used for TTL=0 messages that force a reconnect. They shouldn't be persisted
+ * in the RMQ, but they should be sent if the reconnect is successful.
+ */
+- (void)sendOnConnectOrDrop:(GPBMessage *)message;
+
+@end
diff --git a/Firebase/Messaging/FIRMessagingConnection.m b/Firebase/Messaging/FIRMessagingConnection.m
new file mode 100644
index 0000000..afbd0ba
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingConnection.m
@@ -0,0 +1,711 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRMessagingConnection.h"
+
+#import "Protos/GtalkCore.pbobjc.h"
+#import "Protos/GtalkExtensions.pbobjc.h"
+
+#import "FIRMessaging.h"
+#import "FIRMessagingDataMessageManager.h"
+#import "FIRMessagingDefines.h"
+#import "FIRMessagingLogger.h"
+#import "FIRMessagingRmqManager.h"
+#import "FIRMessagingSecureSocket.h"
+#import "FIRMessagingUtilities.h"
+#import "FIRMessagingVersionUtilities.h"
+#import "FIRMessaging_Private.h"
+
+static NSInteger const kIqSelectiveAck = 12;
+static NSInteger const kIqStreamAck = 13;
+static int const kInvalidStreamId = -1;
+// Threshold for number of messages removed that we will ack, for short lived connections
+static int const kMessageRemoveAckThresholdCount = 5;
+
+static NSTimeInterval const kHeartbeatInterval = 30.0;
+static NSTimeInterval const kConnectionTimeout = 20.0;
+static int32_t const kAckingInterval = 10;
+
+static NSString *const kUnackedS2dIdKey = @"FIRMessagingUnackedS2dIdKey";
+static NSString *const kAckedS2dIdMapKey = @"FIRMessagingAckedS2dIdMapKey";
+
+static NSString *const kRemoteFromAddress = @"from";
+
+@interface FIRMessagingD2SInfo : NSObject
+
+@property(nonatomic, readwrite, assign) int streamId;
+@property(nonatomic, readwrite, strong) NSString *d2sID;
+- (instancetype)initWithStreamId:(int)streamId d2sId:(NSString *)d2sID;
+
+@end
+
+@implementation FIRMessagingD2SInfo
+
+- (instancetype)initWithStreamId:(int)streamId d2sId:(NSString *)d2sID {
+ self = [super init];
+ if (self) {
+ _streamId = streamId;
+ _d2sID = [d2sID copy];
+ }
+ return self;
+}
+
+- (BOOL)isEqual:(id)object {
+ if ([object isKindOfClass:[self class]]) {
+ FIRMessagingD2SInfo *other = (FIRMessagingD2SInfo *)object;
+ return self.streamId == other.streamId && [self.d2sID isEqualToString:other.d2sID];
+ }
+ return NO;
+}
+
+- (NSUInteger)hash {
+ return [self.d2sID hash];
+}
+
+@end
+
+@interface FIRMessagingConnection ()<FIRMessagingSecureSocketDelegate>
+
+@property(nonatomic, readwrite, weak) FIRMessagingRmqManager *rmq2Manager;
+@property(nonatomic, readwrite, weak) FIRMessagingDataMessageManager *dataMessageManager;
+
+@property(nonatomic, readwrite, assign) FIRMessagingConnectionState state;
+@property(nonatomic, readwrite, copy) NSString *host;
+@property(nonatomic, readwrite, assign) NSUInteger port;
+
+@property(nonatomic, readwrite, strong) NSString *authId;
+@property(nonatomic, readwrite, strong) NSString *token;
+
+@property(nonatomic, readwrite, strong) FIRMessagingSecureSocket *socket;
+
+@property(nonatomic, readwrite, assign) int64_t lastLoginServerTimestamp;
+@property(nonatomic, readwrite, assign) int lastStreamIdAcked;
+@property(nonatomic, readwrite, assign) int inStreamId;
+@property(nonatomic, readwrite, assign) int outStreamId;
+
+@property(nonatomic, readwrite, strong) NSMutableArray *unackedS2dIds;
+@property(nonatomic, readwrite, strong) NSMutableDictionary *ackedS2dMap;
+@property(nonatomic, readwrite, strong) NSMutableArray *d2sInfos;
+// ttl=0 messages that need to be sent as soon as we establish a connection
+@property(nonatomic, readwrite, strong) NSMutableArray *sendOnConnectMessages;
+
+@property(nonatomic, readwrite, strong) NSRunLoop *runLoop;
+
+@end
+
+
+@implementation FIRMessagingConnection;
+
+- (instancetype)initWithAuthID:(NSString *)authId
+ token:(NSString *)token
+ host:(NSString *)host
+ port:(NSUInteger)port
+ runLoop:(NSRunLoop *)runLoop
+ rmq2Manager:(FIRMessagingRmqManager *)rmq2Manager
+ fcmManager:(FIRMessagingDataMessageManager *)dataMessageManager {
+ self = [super init];
+ if (self) {
+ _authId = [authId copy];
+ _token = [token copy];
+ _host = [host copy];
+ _port = port;
+ _runLoop = runLoop;
+ _rmq2Manager = rmq2Manager;
+ _dataMessageManager = dataMessageManager;
+
+ _d2sInfos = [NSMutableArray array];
+
+ _unackedS2dIds = [NSMutableArray arrayWithArray:[_rmq2Manager unackedS2dRmqIds]];
+ _ackedS2dMap = [NSMutableDictionary dictionary];
+ _sendOnConnectMessages = [NSMutableArray array];
+ }
+ return self;
+}
+
+- (NSString *)description {
+ return [NSString stringWithFormat:@"host: %@, port: %lu, stream id in: %d, stream id out: %d",
+ self.host,
+ _FIRMessaging_UL(self.port),
+ self.inStreamId,
+ self.outStreamId];
+}
+
+- (void)signIn {
+ _FIRMessagingDevAssert(self.state == kFIRMessagingConnectionNotConnected, @"Invalid connection state.");
+ if (self.state != kFIRMessagingConnectionNotConnected) {
+ return;
+ }
+
+ // break it up for testing
+ [self setupConnectionSocket];
+ [self connectToSocket:self.socket];
+}
+
+- (void)setupConnectionSocket {
+ self.socket = [[FIRMessagingSecureSocket alloc] init];
+ self.socket.delegate = self;
+}
+
+- (void)connectToSocket:(FIRMessagingSecureSocket *)socket {
+ self.state = kFIRMessagingConnectionConnecting;
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeConnection000,
+ @"Start connecting to FIRMessaging service.");
+ [socket connectToHost:self.host port:self.port onRunLoop:self.runLoop];
+}
+
+- (void)signOut {
+ // Clear the list of messages to be sent on connect. This will only
+ // have messages in it if an error happened before receiving the LoginResponse.
+ [self.sendOnConnectMessages removeAllObjects];
+
+ if (self.state == kFIRMessagingConnectionSignedIn) {
+ [self sendClose];
+ }
+ if (self.state != kFIRMessagingConnectionNotConnected) {
+ [self disconnect];
+ }
+}
+
+- (void)teardown {
+ if (self.state != kFIRMessagingConnectionNotConnected) {
+ [self disconnect];
+ }
+}
+
+#pragma mark - FIRMessagingSecureSocketDelegate
+
+- (void)secureSocketDidConnect:(FIRMessagingSecureSocket *)socket {
+ self.state = kFIRMessagingConnectionConnected;
+ self.lastStreamIdAcked = 0;
+ self.inStreamId = 0;
+ self.outStreamId = 0;
+
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeConnection001,
+ @"Connected to FIRMessaging service.");
+ [self resetUnconfirmedAcks];
+ [self sendLoginRequest:self.authId token:self.token];
+}
+
+- (void)didDisconnectWithSecureSocket:(FIRMessagingSecureSocket *)socket {
+ _FIRMessagingDevAssert(self.socket == socket, @"Invalid socket");
+ _FIRMessagingDevAssert(self.socket.state == kFIRMessagingSecureSocketClosed, @"Socket already closed");
+
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeConnection002,
+ @"Secure socket disconnected from FIRMessaging service.");
+ [self disconnect];
+ [self.delegate connection:self didCloseForReason:kFIRMessagingConnectionCloseReasonSocketDisconnected];
+}
+
+- (void)secureSocket:(FIRMessagingSecureSocket *)socket
+ didReceiveData:(NSData *)data
+ withTag:(int8_t)tag {
+ if (tag < 0) {
+ // Invalid proto tag
+ return;
+ }
+
+ Class klassForTag = FIRMessagingGetClassForTag((FIRMessagingProtoTag)tag);
+ if ([klassForTag isSubclassOfClass:[NSNull class]]) {
+ FIRMessagingLoggerError(kFIRMessagingMessageCodeConnection003, @"Invalid tag %d for proto",
+ tag);
+ return;
+ }
+
+ GPBMessage *proto = [klassForTag parseFromData:data error:NULL];
+ if (tag == kFIRMessagingProtoTagLoginResponse && self.state != kFIRMessagingConnectionConnected) {
+ FIRMessagingLoggerDebug(
+ kFIRMessagingMessageCodeConnection004,
+ @"Should not receive generated message when the connection is not connected.");
+ return;
+ } else if (tag != kFIRMessagingProtoTagLoginResponse && self.state != kFIRMessagingConnectionSignedIn) {
+ FIRMessagingLoggerDebug(
+ kFIRMessagingMessageCodeConnection005,
+ @"Should not receive generated message when the connection is not signed in.");
+ return;
+ }
+
+ // If traffic is received after a heartbeat it is safe to assume the connection is healthy.
+ [self cancelConnectionTimeoutTask];
+ [self performSelector:@selector(sendHeartbeatPing)
+ withObject:nil
+ afterDelay:kHeartbeatInterval];
+
+ [self willProcessProto:proto];
+ switch (tag) {
+ case kFIRMessagingProtoTagLoginResponse:
+ [self didReceiveLoginResponse:(GtalkLoginResponse *)proto];
+ break;
+ case kFIRMessagingProtoTagDataMessageStanza:
+ [self didReceiveDataMessageStanza:(GtalkDataMessageStanza *)proto];
+ break;
+ case kFIRMessagingProtoTagHeartbeatPing:
+ [self didReceiveHeartbeatPing:(GtalkHeartbeatPing *)proto];
+ break;
+ case kFIRMessagingProtoTagHeartbeatAck:
+ [self didReceiveHeartbeatAck:(GtalkHeartbeatAck *)proto];
+ break;
+ case kFIRMessagingProtoTagClose:
+ [self didReceiveClose:(GtalkClose *)proto];
+ break;
+ case kFIRMessagingProtoTagIqStanza:
+ [self handleIqStanza:(GtalkIqStanza *)proto];
+ break;
+ default:
+ [self didReceiveUnhandledProto:proto];
+ break;
+ }
+}
+
+// Called from secure socket once we have send the proto with given rmqId over the wire
+// since we are mostly concerned with user facing messages which certainly have a rmqId
+// we can retrieve them from the Rmq if necessary to look at stuff but for now we just
+// log it.
+- (void)secureSocket:(FIRMessagingSecureSocket *)socket
+ didSendProtoWithTag:(int8_t)tag
+ rmqId:(NSString *)rmqId {
+ // log the message
+ [self logMessage:rmqId messageType:tag isOut:YES];
+}
+
+#pragma mark - FIRMessagingTestConnection
+
+- (void)sendProto:(GPBMessage *)proto {
+ FIRMessagingProtoTag tag = FIRMessagingGetTagForProto(proto);
+ if (tag == kFIRMessagingProtoTagLoginRequest && self.state != kFIRMessagingConnectionConnected) {
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeConnection006,
+ @"Cannot send generated message when the connection is not connected.");
+ return;
+ } else if (tag != kFIRMessagingProtoTagLoginRequest && self.state != kFIRMessagingConnectionSignedIn) {
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeConnection007,
+ @"Cannot send generated message when the connection is not signed in.");
+ return;
+ }
+
+ _FIRMessagingDevAssert(self.socket != nil, @"Socket shouldn't be nil");
+ if (self.socket == nil) {
+ return;
+ }
+
+ [self willSendProto:proto];
+
+ [self.socket sendData:proto.data withTag:tag rmqId:FIRMessagingGetRmq2Id(proto)];
+}
+
+- (void)sendOnConnectOrDrop:(GPBMessage *)message {
+ if (self.state == kFIRMessagingConnectionSignedIn) {
+ // If a connection has already been established, send normally
+ [self sendProto:message];
+ } else {
+ // Otherwise add them to the list of messages to send after login
+ [self.sendOnConnectMessages addObject:message];
+ }
+}
+
++ (GtalkLoginRequest *)loginRequestWithToken:(NSString *)token authID:(NSString *)authID {
+ GtalkLoginRequest *login = [[GtalkLoginRequest alloc] init];
+ login.accountId = 1000000;
+ login.authService = GtalkLoginRequest_AuthService_AndroidId;
+ login.authToken = token;
+ login.id_p = [NSString stringWithFormat:@"%@-%@", @"ios", FIRMessagingCurrentLibraryVersion()];
+ login.domain = @"mcs.android.com";
+ login.deviceId = [NSString stringWithFormat:@"android-%llx", authID.longLongValue];
+ login.networkType = [self currentNetworkType];
+ login.resource = authID;
+ login.user = authID;
+ login.useRmq2 = YES;
+ login.lastRmqId = 1; // Sending not enabled yet so this stays as 1.
+ return login;
+}
+
++ (int32_t)currentNetworkType {
+ // http://developer.android.com/reference/android/net/ConnectivityManager.html
+ int32_t fcmNetworkType;
+ FIRMessagingNetworkStatus type = [[FIRMessaging messaging] networkType];
+ switch (type) {
+ case kFIRMessagingReachabilityReachableViaWiFi:
+ fcmNetworkType = 1;
+ break;
+
+ case kFIRMessagingReachabilityReachableViaWWAN:
+ fcmNetworkType = 0;
+ break;
+
+ default:
+ fcmNetworkType = -1;
+ break;
+ }
+ return fcmNetworkType;
+}
+
+- (void)sendLoginRequest:(NSString *)authId
+ token:(NSString *)token {
+ GtalkLoginRequest *login = [[self class] loginRequestWithToken:token authID:authId];
+
+ // clear the messages sent during last connection
+ if ([self.d2sInfos count]) {
+ [self.d2sInfos removeAllObjects];
+ }
+
+ if (self.unackedS2dIds.count > 0) {
+ FIRMessagingLoggerDebug(
+ kFIRMessagingMessageCodeConnection008,
+ @"There are unacked persistent Ids in the login request: %@",
+ [self.unackedS2dIds.description stringByReplacingOccurrencesOfString:@"%"
+ withString:@"%%"]);
+ }
+ // Send out acks.
+ for (NSString *unackedPersistentS2dId in self.unackedS2dIds) {
+ [login.receivedPersistentIdArray addObject:unackedPersistentS2dId];
+ }
+
+ GtalkSetting *setting = [[GtalkSetting alloc] init];
+ setting.name = @"new_vc";
+ setting.value = @"1";
+ [login.settingArray addObject:setting];
+
+ [self sendProto:login];
+}
+
+- (void)sendHeartbeatAck {
+ [self sendProto:[[GtalkHeartbeatAck alloc] init]];
+}
+
+- (void)sendHeartbeatPing {
+ // cancel the previous heartbeat request.
+ [NSObject cancelPreviousPerformRequestsWithTarget:self
+ selector:@selector(sendHeartbeatPing)
+ object:nil];
+ [self scheduleConnectionTimeoutTask];
+ [self sendProto:[[GtalkHeartbeatPing alloc] init]];
+}
+
++ (GtalkIqStanza *)createStreamAck {
+ GtalkIqStanza *iq = [[GtalkIqStanza alloc] init];
+ iq.type = GtalkIqStanza_IqType_Set;
+ iq.id_p = @"";
+ GtalkExtension *ext = [[GtalkExtension alloc] init];
+ ext.id_p = kIqStreamAck;
+ ext.data_p = @"";
+ iq.extension = ext;
+ return iq;
+}
+
+- (void)sendStreamAck {
+ GtalkIqStanza *iq = [[self class] createStreamAck];
+ [self sendProto:iq];
+}
+
+- (void)sendClose {
+ [self sendProto:[[GtalkClose alloc] init]];
+}
+
+- (void)handleIqStanza:(GtalkIqStanza *)iq {
+ if (iq.hasExtension) {
+ if (iq.extension.id_p == kIqStreamAck) {
+ [self didReceiveStreamAck:iq];
+ return;
+ }
+ if (iq.extension.id_p == kIqSelectiveAck) {
+ [self didReceiveSelectiveAck:iq];
+ return;
+ }
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeConnection009, @"Unknown ack extension id %d.",
+ iq.extension.id_p);
+ } else {
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeConnection010, @"Ip stanza without extension.");
+ }
+ [self didReceiveUnhandledProto:iq];
+}
+
+- (void)didReceiveLoginResponse:(GtalkLoginResponse *)loginResponse {
+ if (loginResponse.hasError) {
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeConnection011,
+ @"Login error with type: %@, message: %@.", loginResponse.error.type,
+ loginResponse.error.message);
+ return;
+ }
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeConnection012, @"Logged onto MCS service.");
+ // We sent the persisted list of unack'd messages with login so we can assume they have been ack'd
+ // by the server.
+ _FIRMessagingDevAssert(self.unackedS2dIds.count == 0, @"No ids present");
+ _FIRMessagingDevAssert(self.outStreamId == 1, @"Login should be the first stream id");
+
+ self.state = kFIRMessagingConnectionSignedIn;
+ self.lastLoginServerTimestamp = loginResponse.serverTimestamp;
+ [self.delegate didLoginWithConnection:self];
+ [self sendHeartbeatPing];
+
+ // Add all the TTL=0 messages on connect
+ for (GPBMessage *message in self.sendOnConnectMessages) {
+ [self sendProto:message];
+ }
+ [self.sendOnConnectMessages removeAllObjects];
+}
+
+- (void)didReceiveHeartbeatPing:(GtalkHeartbeatPing *)heartbeatPing {
+ [self sendHeartbeatAck];
+}
+
+- (void)didReceiveHeartbeatAck:(GtalkHeartbeatAck *)heartbeatAck {
+#if FIRMessaging_PROBER
+ self.lastHeartbeatPingTimestamp = FIRMessagingCurrentTimestampInSeconds();
+#endif
+}
+
+- (void)didReceiveDataMessageStanza:(GtalkDataMessageStanza *)dataMessageStanza {
+ // TODO: Maybe add support raw data later
+ [self.delegate connectionDidRecieveMessage:dataMessageStanza];
+}
+
+- (void)didReceiveUnhandledProto:(GPBMessage *)proto {
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeConnection013, @"Received unhandled proto");
+}
+
+- (void)didReceiveStreamAck:(GtalkIqStanza *)iq {
+ // Server received some stuff from us we don't really need to do anything special
+}
+
+- (void)didReceiveSelectiveAck:(GtalkIqStanza *)iq {
+ GtalkExtension *extension = iq.extension;
+ if (extension) {
+ int extensionId = extension.id_p;
+ if (extensionId == kIqSelectiveAck) {
+
+ NSString *dataString = extension.data_p;
+ GtalkSelectiveAck *selectiveAck = [[GtalkSelectiveAck alloc] init];
+ [selectiveAck mergeFromData:[dataString dataUsingEncoding:NSUTF8StringEncoding]
+ extensionRegistry:nil];
+
+ NSArray <NSString *>*acks = [selectiveAck idArray];
+
+ // we've received ACK's
+ [self.delegate connectionDidReceiveAckForRmqIds:acks];
+
+ // resend unacked messages
+ [self.dataMessageManager resendMessagesWithConnection:self];
+ }
+ }
+}
+
+- (void)didReceiveClose:(GtalkClose *)close {
+ [self disconnect];
+}
+
+- (void)willProcessProto:(GPBMessage *)proto {
+ self.inStreamId++;
+
+ if ([proto isKindOfClass:GtalkDataMessageStanza.class]) {
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeConnection014,
+ @"RMQ: Receiving %@ with rmq_id: %@ incoming stream Id: %d",
+ proto.class, FIRMessagingGetRmq2Id(proto), self.inStreamId);
+ } else {
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeConnection015,
+ @"RMQ: Receiving %@ with incoming stream Id: %d.", proto.class,
+ self.inStreamId);
+ }
+ int streamId = FIRMessagingGetLastStreamId(proto);
+ if (streamId != kInvalidStreamId) {
+ // confirm the D2S messages that were sent by us
+ [self confirmAckedD2sIdsWithStreamId:streamId];
+
+ // We can now confirm that our ack was received by the server and start our unack'd list fresh
+ // with the proto we just received.
+ [self confirmAckedS2dIdsWithStreamId:streamId];
+ }
+ NSString *rmq2Id = FIRMessagingGetRmq2Id(proto);
+ if (rmq2Id != nil) {
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeConnection016,
+ @"RMQ: Add unacked persistent Id: %@.",
+ [rmq2Id stringByReplacingOccurrencesOfString:@"%" withString:@"%%"]);
+ [self.unackedS2dIds addObject:rmq2Id];
+ [self.rmq2Manager saveS2dMessageWithRmqId:rmq2Id]; // RMQ save
+ }
+ BOOL explicitAck = ([proto isKindOfClass:[GtalkDataMessageStanza class]] &&
+ [(GtalkDataMessageStanza *)proto immediateAck]);
+ // If we have not sent anything and the ack threshold has been reached then explicitly send one
+ // to notify the server that we have received messages.
+ if (self.inStreamId - self.lastStreamIdAcked >= kAckingInterval || explicitAck) {
+ [self sendStreamAck];
+ }
+}
+
+- (void)willSendProto:(GPBMessage *)proto {
+ self.outStreamId++;
+
+ NSString *rmq2Id = FIRMessagingGetRmq2Id(proto);
+ if ([rmq2Id length]) {
+ FIRMessagingD2SInfo *d2sInfo = [[FIRMessagingD2SInfo alloc] initWithStreamId:self.outStreamId d2sId:rmq2Id];
+ [self.d2sInfos addObject:d2sInfo];
+ }
+
+ // each time we send a d2s message, it acks previously received
+ // s2d messages via the last (s2d) stream id received.
+
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeConnection017,
+ @"RMQ: Sending %@ with outgoing stream Id: %d.", proto.class,
+ self.outStreamId);
+ // We have received messages since last time we sent something - send ack info to server.
+ if (self.inStreamId > self.lastStreamIdAcked) {
+ FIRMessagingSetLastStreamId(proto, self.inStreamId);
+ self.lastStreamIdAcked = self.inStreamId;
+ }
+
+ if (self.unackedS2dIds.count > 0) {
+ // Move all 'unack'd' messages to the ack'd map so they can be removed once the
+ // ack is confirmed.
+ NSArray *ackedS2dIds = [NSArray arrayWithArray:self.unackedS2dIds];
+ FIRMessagingLoggerDebug(
+ kFIRMessagingMessageCodeConnection018, @"RMQ: Mark persistent Ids as acked: %@.",
+ [ackedS2dIds.description stringByReplacingOccurrencesOfString:@"%" withString:@"%%"]);
+ [self.unackedS2dIds removeAllObjects];
+ self.ackedS2dMap[[@(self.outStreamId) stringValue]] = ackedS2dIds;
+ }
+}
+
+#pragma mark - Private
+
+/**
+ * This processes the s2d message received in reference to the d2s messages
+ * that we have sent before.
+ */
+- (void)confirmAckedD2sIdsWithStreamId:(int)lastReceivedStreamId {
+ NSMutableArray *d2sIdsAcked = [NSMutableArray array];
+ for (FIRMessagingD2SInfo *d2sInfo in self.d2sInfos) {
+ if (lastReceivedStreamId < d2sInfo.streamId) {
+ break;
+ }
+ [d2sIdsAcked addObject:d2sInfo];
+ }
+
+ NSMutableArray *rmqIds = [NSMutableArray arrayWithCapacity:[d2sIdsAcked count]];
+ // remove ACK'ed messages
+ for (FIRMessagingD2SInfo *d2sInfo in d2sIdsAcked) {
+ if ([d2sInfo.d2sID length]) {
+ [rmqIds addObject:d2sInfo.d2sID];
+ }
+ [self.d2sInfos removeObject:d2sInfo];
+ }
+ [self.delegate connectionDidReceiveAckForRmqIds:rmqIds];
+ int count = [self.delegate connectionDidReceiveAckForRmqIds:rmqIds];
+ if (kMessageRemoveAckThresholdCount > 0 && count >= kMessageRemoveAckThresholdCount) {
+ // For short lived connections, if a large number of messages are removed, send an
+ // ack straight away so the server knows that this message was received.
+ [self sendStreamAck];
+ }
+}
+
+/**
+ * Called when a stream ACK or a selective ACK are received - this indicates the message has
+ * been received by MCS.
+ */
+- (void)didReceiveAckForRmqIds:(NSArray *)rmqIds {
+ // TODO: let the user know that the following messages were received by the server
+}
+
+- (void)confirmAckedS2dIdsWithStreamId:(int)lastReceivedStreamId {
+ // If the server hasn't received the streamId yet.
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeConnection019,
+ @"RMQ: Server last received stream Id: %d.", lastReceivedStreamId);
+ if (lastReceivedStreamId < self.outStreamId) {
+ // TODO: This could be a good indicator that we need to re-send something (acks)?
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeConnection020,
+ @"RMQ: There are unsent messages that should be send...\n"
+ "server received: %d\nlast stream id sent: %d",
+ lastReceivedStreamId, self.outStreamId);
+ }
+
+ NSSet *ackedStreamIds =
+ [self.ackedS2dMap keysOfEntriesPassingTest:^BOOL(id key, id obj, BOOL *stop) {
+ NSString *streamId = key;
+ return streamId.intValue <= lastReceivedStreamId;
+ }];
+ NSMutableArray *s2dIdsToDelete = [NSMutableArray array];
+
+ for (NSString *streamId in ackedStreamIds) {
+ NSArray *ackedS2dIds = self.ackedS2dMap[streamId];
+ if (ackedS2dIds.count > 0) {
+ FIRMessagingLoggerDebug(
+ kFIRMessagingMessageCodeConnection021,
+ @"RMQ: Mark persistent Ids as confirmed by stream id %@: %@.", streamId,
+ [ackedS2dIds.description stringByReplacingOccurrencesOfString:@"%" withString:@"%%"]);
+ [self.ackedS2dMap removeObjectForKey:streamId];
+ }
+
+ [s2dIdsToDelete addObjectsFromArray:ackedS2dIds];
+ }
+
+ // clean up s2d ids that the server knows we've received.
+ // we let the server know via a s2d last stream id received in a
+ // d2s message. the server lets us know it has received our d2s
+ // message via a d2s last stream id received in a s2d message.
+ [self.rmq2Manager removeS2dIds:s2dIdsToDelete];
+}
+
+- (void)resetUnconfirmedAcks {
+ [self.ackedS2dMap enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
+ [self.unackedS2dIds addObjectsFromArray:obj];
+ }];
+ [self.ackedS2dMap removeAllObjects];
+}
+
+- (void)disconnect {
+ _FIRMessagingDevAssert(self.state != kFIRMessagingConnectionNotConnected, @"Connection already not connected");
+ // cancel pending timeout tasks.
+ [self cancelConnectionTimeoutTask];
+ // cancel pending heartbeat.
+ [NSObject cancelPreviousPerformRequestsWithTarget:self
+ selector:@selector(sendHeartbeatPing)
+ object:nil];
+ // Unset the delegate. FIRMessagingConnection will not receive further events from the socket from now on.
+ self.socket.delegate = nil;
+ [self.socket disconnect];
+ self.state = kFIRMessagingConnectionNotConnected;
+}
+
+- (void)connectionTimedOut {
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeConnection022,
+ @"Connection to FIRMessaging service timed out.");
+ [self disconnect];
+ [self.delegate connection:self didCloseForReason:kFIRMessagingConnectionCloseReasonTimeout];
+}
+
+- (void)scheduleConnectionTimeoutTask {
+ // cancel the previous heartbeat timeout event and schedule a new one.
+ [self cancelConnectionTimeoutTask];
+ [self performSelector:@selector(connectionTimedOut)
+ withObject:nil
+ afterDelay:[self connectionTimeoutInterval]];
+}
+
+- (void)cancelConnectionTimeoutTask {
+ // cancel pending timeout tasks.
+ [NSObject cancelPreviousPerformRequestsWithTarget:self
+ selector:@selector(connectionTimedOut)
+ object:nil];
+}
+
+- (void)logMessage:(NSString *)description messageType:(int)messageType isOut:(BOOL)isOut {
+ messageType = isOut ? -messageType : messageType;
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeConnection023,
+ @"Send msg: %@ type: %d inStreamId: %d outStreamId: %d", description,
+ messageType, self.inStreamId, self.outStreamId);
+}
+
+- (NSTimeInterval)connectionTimeoutInterval {
+ return kConnectionTimeout;
+}
+
+@end
diff --git a/Firebase/Messaging/FIRMessagingConstants.h b/Firebase/Messaging/FIRMessagingConstants.h
new file mode 100644
index 0000000..0e244a5
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingConstants.h
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Global constants to be put here.
+ *
+ */
+#import <Foundation/Foundation.h>
+
+#ifndef _FIRMessaging_CONSTANTS_H
+#define _FIRMessaging_CONSTANTS_H
+
+FOUNDATION_EXPORT NSString *const kFIRMessagingRawDataKey;
+FOUNDATION_EXPORT NSString *const kFIRMessagingCollapseKey;
+FOUNDATION_EXPORT NSString *const kFIRMessagingFromKey;
+
+FOUNDATION_EXPORT NSString *const kFIRMessagingSendTo;
+FOUNDATION_EXPORT NSString *const kFIRMessagingSendTTL;
+FOUNDATION_EXPORT NSString *const kFIRMessagingSendDelay;
+FOUNDATION_EXPORT NSString *const kFIRMessagingSendMessageID;
+FOUNDATION_EXPORT NSString *const KFIRMessagingSendMessageAppData;
+
+FOUNDATION_EXPORT NSString *const kFIRMessagingMessageInternalReservedKeyword;
+FOUNDATION_EXPORT NSString *const kFIRMessagingMessagePersistentIDKey;
+
+FOUNDATION_EXPORT NSString *const kFIRMessagingMessageIDKey;
+FOUNDATION_EXPORT NSString *const kFIRMessagingMessageAPNSContentAvailableKey;
+FOUNDATION_EXPORT NSString *const kFIRMessagingMessageSyncViaMCSKey;
+FOUNDATION_EXPORT NSString *const kFIRMessagingMessageSyncMessageTTLKey;
+FOUNDATION_EXPORT NSString *const kFIRMessagingMessageLinkKey;
+
+FOUNDATION_EXPORT NSString *const kFIRMessagingLibraryVersion;
+
+FOUNDATION_EXPORT NSString *const kFIRMessagingRemoteNotificationsProxyEnabledInfoPlistKey;
+
+FOUNDATION_EXPORT NSString *const kFIRMessagingApplicationSupportSubDirectory;
+
+// Notifications
+FOUNDATION_EXPORT NSString *const kFIRMessagingAPNSTokenNotification;
+FOUNDATION_EXPORT NSString *const kFIRMessagingFCMTokenNotification;
+FOUNDATION_EXPORT NSString *const kFIRMessagingInstanceIDTokenRefreshNotification;
+
+FOUNDATION_EXPORT const int kFIRMessagingSendTtlDefault; // 24 hours
+
+#endif
diff --git a/Firebase/Messaging/FIRMessagingConstants.m b/Firebase/Messaging/FIRMessagingConstants.m
new file mode 100644
index 0000000..f8e420c
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingConstants.m
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRMessagingConstants.h"
+
+NSString *const kFIRMessagingRawDataKey = @"rawData";
+NSString *const kFIRMessagingCollapseKey = @"collapse_key";
+NSString *const kFIRMessagingFromKey = @"from";
+
+NSString *const kFIRMessagingSendTo = @"google." @"to";
+NSString *const kFIRMessagingSendTTL = @"google." @"ttl";
+NSString *const kFIRMessagingSendDelay = @"google." @"delay";
+NSString *const kFIRMessagingSendMessageID = @"google." @"msg_id";
+NSString *const KFIRMessagingSendMessageAppData = @"google." @"data";
+
+NSString *const kFIRMessagingMessageInternalReservedKeyword = @"gcm.";
+NSString *const kFIRMessagingMessagePersistentIDKey = @"persistent_id";
+
+NSString *const kFIRMessagingMessageIDKey = @"gcm." @"message_id";
+NSString *const kFIRMessagingMessageAPNSContentAvailableKey = @"content-available";
+NSString *const kFIRMessagingMessageSyncViaMCSKey = @"gcm." @"duplex";
+NSString *const kFIRMessagingMessageSyncMessageTTLKey = @"gcm." @"ttl";
+NSString *const kFIRMessagingMessageLinkKey = @"gcm." @"app_link";
+
+NSString *const kFIRMessagingLibraryVersion = @"FIRMessaging-version";
+
+NSString *const kFIRMessagingRemoteNotificationsProxyEnabledInfoPlistKey =
+ @"FirebaseAppDelegateProxyEnabled";
+
+NSString *const kFIRMessagingApplicationSupportSubDirectory = @"Google/FirebaseMessaging";
+
+// Notifications
+NSString *const kFIRMessagingAPNSTokenNotification = @"com.firebase.iid.notif.apns-token";
+NSString *const kFIRMessagingFCMTokenNotification = @"com.firebase.iid.notif.fcm-token";
+NSString *const kFIRMessagingInstanceIDTokenRefreshNotification =
+ @"com.firebase.iid.notif.refresh-token";
+
+const int kFIRMessagingSendTtlDefault = 24 * 60 * 60; // 24 hours
diff --git a/Firebase/Messaging/FIRMessagingContextManagerService.h b/Firebase/Messaging/FIRMessagingContextManagerService.h
new file mode 100644
index 0000000..83e6444
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingContextManagerService.h
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+FOUNDATION_EXPORT NSString *const kFIRMessagingContextManagerCategory;
+FOUNDATION_EXPORT NSString *const kFIRMessagingContextManagerLocalTimeStart;
+FOUNDATION_EXPORT NSString *const kFIRMessagingContextManagerLocalTimeEnd;
+FOUNDATION_EXPORT NSString *const kFIRMessagingContextManagerBodyKey;
+
+@interface FIRMessagingContextManagerService : NSObject
+
+/**
+ * Check if the message is a context manager message or not.
+ *
+ * @param message The message to verify.
+ *
+ * @return YES if the message is a context manager message else NO.
+ */
++ (BOOL)isContextManagerMessage:(NSDictionary *)message;
+
+/**
+ * Handle context manager message.
+ *
+ * @param message The message to handle.
+ *
+ * @return YES if the message was handled successfully else NO.
+ */
++ (BOOL)handleContextManagerMessage:(NSDictionary *)message;
+
+@end
diff --git a/Firebase/Messaging/FIRMessagingContextManagerService.m b/Firebase/Messaging/FIRMessagingContextManagerService.m
new file mode 100644
index 0000000..1c9f653
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingContextManagerService.m
@@ -0,0 +1,189 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRMessagingContextManagerService.h"
+
+#import <UIKit/UIKit.h>
+
+#import "FIRMessagingDefines.h"
+#import "FIRMessagingLogger.h"
+
+#define kFIRMessagingContextManagerPrefixKey @"google.c.cm."
+#define kFIRMessagingContextManagerNotificationKeyPrefix @"gcm.notification."
+
+static NSString *const kLogTag = @"FIRMessagingAnalytics";
+
+static NSString *const kLocalTimeFormatString = @"yyyy-MM-dd HH:mm:ss";
+
+static NSString *const kContextManagerPrefixKey = kFIRMessagingContextManagerPrefixKey;
+
+// Local timed messages (format yyyy-mm-dd HH:mm:ss)
+NSString *const kFIRMessagingContextManagerLocalTimeStart = kFIRMessagingContextManagerPrefixKey @"lt_start";
+NSString *const kFIRMessagingContextManagerLocalTimeEnd = kFIRMessagingContextManagerPrefixKey @"lt_end";
+
+// Local Notification Params
+NSString *const kFIRMessagingContextManagerBodyKey = kFIRMessagingContextManagerNotificationKeyPrefix @"body";
+NSString *const kFIRMessagingContextManagerTitleKey = kFIRMessagingContextManagerNotificationKeyPrefix @"title";
+NSString *const kFIRMessagingContextManagerBadgeKey = kFIRMessagingContextManagerNotificationKeyPrefix @"badge";
+NSString *const kFIRMessagingContextManagerCategoryKey =
+ kFIRMessagingContextManagerNotificationKeyPrefix @"click_action";
+NSString *const kFIRMessagingContextManagerSoundKey = kFIRMessagingContextManagerNotificationKeyPrefix @"sound";
+NSString *const kFIRMessagingContextManagerContentAvailableKey =
+ kFIRMessagingContextManagerNotificationKeyPrefix @"content-available";
+
+typedef NS_ENUM(NSUInteger, FIRMessagingContextManagerMessageType) {
+ FIRMessagingContextManagerMessageTypeNone,
+ FIRMessagingContextManagerMessageTypeLocalTime,
+};
+
+@implementation FIRMessagingContextManagerService
+
++ (BOOL)isContextManagerMessage:(NSDictionary *)message {
+ // For now we only support local time in ContextManager.
+ if (![message[kFIRMessagingContextManagerLocalTimeStart] length]) {
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeContextManagerService000,
+ @"Received message missing local start time, dropped.");
+ return NO;
+ }
+
+ return YES;
+}
+
++ (BOOL)handleContextManagerMessage:(NSDictionary *)message {
+ NSString *startTimeString = message[kFIRMessagingContextManagerLocalTimeStart];
+ if (startTimeString.length) {
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeContextManagerService001,
+ @"%@ Received context manager message with local time %@", kLogTag,
+ startTimeString);
+ return [self handleContextManagerLocalTimeMessage:message];
+ }
+
+ return NO;
+}
+
++ (BOOL)handleContextManagerLocalTimeMessage:(NSDictionary *)message {
+ NSString *startTimeString = message[kFIRMessagingContextManagerLocalTimeStart];
+ NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
+ [dateFormatter setDateFormat:kLocalTimeFormatString];
+ NSDate *startDate = [dateFormatter dateFromString:startTimeString];
+
+ _FIRMessagingDevAssert(startDate, @"Invalid local start date format %@", startTimeString);
+ if (!startTimeString) {
+ FIRMessagingLoggerError(kFIRMessagingMessageCodeContextManagerService002,
+ @"Invalid local start date format %@. Message dropped",
+ startTimeString);
+ return NO;
+ }
+
+ NSDate *currentDate = [NSDate date];
+
+ if ([currentDate compare:startDate] == NSOrderedAscending) {
+ [self scheduleLocalNotificationForMessage:message
+ atDate:startDate];
+ } else {
+ // check end time has not passed
+ NSString *endTimeString = message[kFIRMessagingContextManagerLocalTimeEnd];
+ if (!endTimeString) {
+ FIRMessagingLoggerInfo(
+ kFIRMessagingMessageCodeContextManagerService003,
+ @"No end date specified for message, start date elapsed. Message dropped.");
+ return YES;
+ }
+
+ NSDate *endDate = [dateFormatter dateFromString:endTimeString];
+
+ _FIRMessagingDevAssert(endDate, @"Invalid local end date format %@", endTimeString);
+ if (!endTimeString) {
+ FIRMessagingLoggerError(kFIRMessagingMessageCodeContextManagerService004,
+ @"Invalid local end date format %@. Message dropped", endTimeString);
+ return NO;
+ }
+
+ if ([endDate compare:currentDate] == NSOrderedAscending) {
+ // end date has already passed drop the message
+ FIRMessagingLoggerInfo(kFIRMessagingMessageCodeContextManagerService005,
+ @"End date %@ has already passed. Message dropped.", endTimeString);
+ return YES;
+ }
+
+ // schedule message right now (buffer 10s)
+ [self scheduleLocalNotificationForMessage:message
+ atDate:[currentDate dateByAddingTimeInterval:10]];
+ }
+ return YES;
+}
+
++ (void)scheduleLocalNotificationForMessage:(NSDictionary *)message
+ atDate:(NSDate *)date {
+ NSDictionary *apsDictionary = message;
+ UILocalNotification *notification = [[UILocalNotification alloc] init];
+
+ // A great way to understand timezones and UILocalNotifications
+ // http://stackoverflow.com/questions/18424569/understanding-uilocalnotification-timezone
+ notification.timeZone = [NSTimeZone defaultTimeZone];
+ notification.fireDate = date;
+
+ // In the current solution all of the display stuff goes into a special "aps" dictionary
+ // being sent in the message.
+ if ([apsDictionary[kFIRMessagingContextManagerBodyKey] length]) {
+ notification.alertBody = apsDictionary[kFIRMessagingContextManagerBodyKey];
+ }
+ if ([apsDictionary[kFIRMessagingContextManagerTitleKey] length]) {
+ // |alertTitle| is iOS 8.2+, so check if we can set it
+ if ([notification respondsToSelector:@selector(setAlertTitle:)]) {
+ notification.alertTitle = apsDictionary[kFIRMessagingContextManagerTitleKey];
+ }
+ }
+
+ if (apsDictionary[kFIRMessagingContextManagerSoundKey]) {
+ notification.soundName = apsDictionary[kFIRMessagingContextManagerSoundKey];
+ }
+ if (apsDictionary[kFIRMessagingContextManagerBadgeKey]) {
+ notification.applicationIconBadgeNumber =
+ [apsDictionary[kFIRMessagingContextManagerBadgeKey] integerValue];
+ }
+ if (apsDictionary[kFIRMessagingContextManagerCategoryKey]) {
+ // |category| is iOS 8.0+, so check if we can set it
+ if ([notification respondsToSelector:@selector(setCategory:)]) {
+ notification.category = apsDictionary[kFIRMessagingContextManagerCategoryKey];
+ }
+ }
+
+ NSDictionary *userInfo = [self parseDataFromMessage:message];
+ if (userInfo.count) {
+ notification.userInfo = userInfo;
+ }
+
+ [[UIApplication sharedApplication] scheduleLocalNotification:notification];
+}
+
++ (NSDictionary *)parseDataFromMessage:(NSDictionary *)message {
+ NSMutableDictionary *data = [NSMutableDictionary dictionary];
+ for (NSObject<NSCopying> *key in message) {
+ if ([key isKindOfClass:[NSString class]]) {
+ NSString *keyString = (NSString *)key;
+ if ([keyString isEqualToString:kFIRMessagingContextManagerContentAvailableKey]) {
+ continue;
+ } else if ([keyString hasPrefix:kContextManagerPrefixKey]) {
+ continue;
+ }
+ }
+ data[[key copy]] = message[key];
+ }
+ return [data copy];
+}
+
+@end
diff --git a/Firebase/Messaging/FIRMessagingDataMessageManager.h b/Firebase/Messaging/FIRMessagingDataMessageManager.h
new file mode 100644
index 0000000..8eaecc1
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingDataMessageManager.h
@@ -0,0 +1,101 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+@class GtalkDataMessageStanza;
+
+@class FIRMessagingClient;
+@class FIRMessagingConnection;
+@class FIRMessagingReceiver;
+@class FIRMessagingRmqManager;
+@class FIRMessagingSyncMessageManager;
+
+@protocol FIRMessagingDataMessageManagerDelegate <NSObject>
+
+#pragma mark - Downstream Callbacks
+
+/**
+ * Invoked when FIRMessaging receives a downstream message via the MCS connection.
+ * Let's the user know that they have received a new message by invoking the
+ * App's remoteNotification callback.
+ *
+ * @param message The downstream message received by the MCS connection.
+ */
+- (void)didReceiveMessage:(nonnull NSDictionary *)message
+ withIdentifier:(nullable NSString *)messageID;
+
+#pragma mark - Upstream Callbacks
+
+/**
+ * Notify the app that FIRMessaging will soon be sending the upstream message requested by the app.
+ *
+ * @param messageID The messageId passed in by the app to track this particular message.
+ * @param error The error in case FIRMessaging cannot send the message upstream.
+ */
+- (void)willSendDataMessageWithID:(nonnull NSString *)messageID error:(nullable NSError *)error;
+
+/**
+ * Notify the app that FIRMessaging did successfully send it's message via the MCS
+ * connection and the message was successfully delivered.
+ *
+ * @param messageId The messageId passed in by the app to track this particular
+ * message.
+ */
+- (void)didSendDataMessageWithID:(nonnull NSString *)messageId;
+
+#pragma mark - Server Callbacks
+
+/**
+ * Notify the app that FIRMessaging server deleted some messages which exceeded storage limits. This
+ * indicates the "deleted_messages" message type we received from the server.
+ */
+- (void)didDeleteMessagesOnServer;
+
+@end
+
+/**
+ * This manages all of the data messages being sent by the client and also the messages that
+ * were received from the server.
+ */
+@interface FIRMessagingDataMessageManager : NSObject
+
+NS_ASSUME_NONNULL_BEGIN
+
+- (instancetype)initWithDelegate:(id<FIRMessagingDataMessageManagerDelegate>)delegate
+ client:(FIRMessagingClient *)client
+ rmq2Manager:(FIRMessagingRmqManager *)rmq2Manager
+ syncMessageManager:(FIRMessagingSyncMessageManager *)syncMessageManager;
+
+- (void)setDeviceAuthID:(NSString *)deviceAuthID secretToken:(NSString *)secretToken;
+
+- (void)refreshDelayedMessages;
+
+#pragma mark - Receive
+
+- (NSDictionary *)processPacket:(GtalkDataMessageStanza *)packet;
+- (void)didReceiveParsedMessage:(NSDictionary *)message;
+
+#pragma mark - Send
+
+- (void)sendDataMessageStanza:(NSMutableDictionary *)dataMessage;
+- (void)didSendDataMessageStanza:(GtalkDataMessageStanza *)message;
+
+- (void)resendMessagesWithConnection:(FIRMessagingConnection *)connection;
+
+NS_ASSUME_NONNULL_END
+
+@end
diff --git a/Firebase/Messaging/FIRMessagingDataMessageManager.m b/Firebase/Messaging/FIRMessagingDataMessageManager.m
new file mode 100644
index 0000000..2433bd4
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingDataMessageManager.m
@@ -0,0 +1,545 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRMessagingDataMessageManager.h"
+
+#import "Protos/GtalkCore.pbobjc.h"
+
+#import "FIRMessagingClient.h"
+#import "FIRMessagingConnection.h"
+#import "FIRMessagingConstants.h"
+#import "FIRMessagingDefines.h"
+#import "FIRMessagingDelayedMessageQueue.h"
+#import "FIRMessagingLogger.h"
+#import "FIRMessagingReceiver.h"
+#import "FIRMessagingRmqManager.h"
+#import "FIRMessaging_Private.h"
+#import "FIRMessagingSyncMessageManager.h"
+#import "FIRMessagingUtilities.h"
+#import "NSError+FIRMessaging.h"
+
+// The Notification used to send InstanceID messages that FIRMessaging receives.
+static NSString *const NOTIFICATION_IID_MESSAGE = @"com.google.gcm/notification/iid";
+
+static const int kMaxAppDataSizeDefault = 4 * 1024; // 4k
+static const int kMinDelaySeconds = 1; // 1 second
+static const int kMaxDelaySeconds = 60 * 60; // 1 hour
+
+static NSString *const kFromForInstanceIDMessages = @"google.com/iid";
+static NSString *const kFromForFIRMessagingMessages = @"mcs.android.com";
+static NSString *const kGSFMessageCategory = @"com.google.android.gsf.gtalkservice";
+// TODO: Update Gcm to FIRMessaging in the constants below
+static NSString *const kFCMMessageCategory = @"com.google.gcm";
+static NSString *const kMessageReservedPrefix = @"google.";
+
+static NSString *const kFCMMessageSpecialMessage = @"message_type";
+
+// special messages sent by the server
+static NSString *const kFCMMessageTypeDeletedMessages = @"deleted_messages";
+
+static NSString *const kMCSNotificationPrefix = @"gcm.notification.";
+static NSString *const kDataMessageNotificationKey = @"notification";
+
+
+typedef NS_ENUM(int8_t, UpstreamForceReconnect) {
+ // Never force reconnect on upstream messages
+ kUpstreamForceReconnectOff = 0,
+ // Force reconnect for TTL=0 upstream messages
+ kUpstreamForceReconnectTTL0 = 1,
+ // Force reconnect for all upstream messages
+ kUpstreamForceReconnectAll = 2,
+};
+
+@interface FIRMessagingDataMessageManager ()
+
+@property(nonatomic, readwrite, weak) FIRMessagingClient *client;
+@property(nonatomic, readwrite, weak) FIRMessagingRmqManager *rmq2Manager;
+@property(nonatomic, readwrite, weak) FIRMessagingSyncMessageManager *syncMessageManager;
+@property(nonatomic, readwrite, weak) id<FIRMessagingDataMessageManagerDelegate> delegate;
+@property(nonatomic, readwrite, strong) FIRMessagingDelayedMessageQueue *delayedMessagesQueue;
+
+@property(nonatomic, readwrite, assign) int ttl;
+@property(nonatomic, readwrite, copy) NSString *deviceAuthID;
+@property(nonatomic, readwrite, copy) NSString *secretToken;
+@property(nonatomic, readwrite, assign) int maxAppDataSize;
+@property(nonatomic, readwrite, assign) UpstreamForceReconnect upstreamForceReconnect;
+
+@end
+
+@implementation FIRMessagingDataMessageManager
+
+- (instancetype)initWithDelegate:(id<FIRMessagingDataMessageManagerDelegate>)delegate
+ client:(FIRMessagingClient *)client
+ rmq2Manager:(FIRMessagingRmqManager *)rmq2Manager
+ syncMessageManager:(FIRMessagingSyncMessageManager *)syncMessageManager {
+ self = [super init];
+ if (self) {
+ _delegate = delegate;
+ _client = client;
+ _rmq2Manager = rmq2Manager;
+ _syncMessageManager = syncMessageManager;
+ _ttl = kFIRMessagingSendTtlDefault;
+ _maxAppDataSize = kMaxAppDataSizeDefault;
+ // on by default
+ _upstreamForceReconnect = kUpstreamForceReconnectAll;
+ }
+ return self;
+}
+
+- (void)setDeviceAuthID:(NSString *)deviceAuthID secretToken:(NSString *)secretToken {
+ _FIRMessagingDevAssert([deviceAuthID length] && [secretToken length],
+ @"Invalid credentials for FIRMessaging");
+ self.deviceAuthID = deviceAuthID;
+ self.secretToken = secretToken;
+}
+
+- (void)refreshDelayedMessages {
+ FIRMessaging_WEAKIFY(self);
+ self.delayedMessagesQueue =
+ [[FIRMessagingDelayedMessageQueue alloc] initWithRmqScanner:self.rmq2Manager
+ sendDelayedMessagesHandler:^(NSArray *messages) {
+ FIRMessaging_STRONGIFY(self);
+ [self sendDelayedMessages:messages];
+ }];
+}
+
+- (NSDictionary *)processPacket:(GtalkDataMessageStanza *)dataMessage {
+ NSString *category = dataMessage.category;
+ NSString *from = dataMessage.from;
+ if ([kFCMMessageCategory isEqualToString:category] ||
+ [kGSFMessageCategory isEqualToString:category]) {
+ [self handleMCSDataMessage:dataMessage];
+ return nil;
+ } else if ([kFromForFIRMessagingMessages isEqualToString:from]) {
+ [self handleMCSDataMessage:dataMessage];
+ return nil;
+ } else if ([kFromForInstanceIDMessages isEqualToString:from]) {
+ // send message to InstanceID library.
+ NSMutableDictionary *message = [NSMutableDictionary dictionary];
+ for (GtalkAppData *item in dataMessage.appDataArray) {
+ _FIRMessagingDevAssert(item.key && item.value, @"Invalid app data item");
+ if (item.key && item.value) {
+ message[item.key] = item.value;
+ }
+ }
+
+ [[NSNotificationCenter defaultCenter] postNotificationName:NOTIFICATION_IID_MESSAGE
+ object:message];
+ return nil;
+ }
+
+ return [self parseDataMessage:dataMessage];
+}
+
+- (void)handleMCSDataMessage:(GtalkDataMessageStanza *)dataMessage {
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeDataMessageManager000,
+ @"Received message for FIRMessaging from downstream %@", dataMessage);
+}
+
+- (NSDictionary *)parseDataMessage:(GtalkDataMessageStanza *)dataMessage {
+ NSMutableDictionary *message = [NSMutableDictionary dictionary];
+ NSString *from = [dataMessage from];
+ if ([from length]) {
+ message[kFIRMessagingFromKey] = from;
+ }
+
+ // raw data
+ NSData *rawData = [dataMessage rawData];
+ if ([rawData length]) {
+ message[kFIRMessagingRawDataKey] = rawData;
+ }
+
+ NSString *token = [dataMessage token];
+ if ([token length]) {
+ message[kFIRMessagingCollapseKey] = token;
+ }
+
+ // Add the persistent_id. This would be removed later before sending the message to the device.
+ NSString *persistentID = [dataMessage persistentId];
+ _FIRMessagingDevAssert([persistentID length], @"Invalid MCS message without persistentID");
+ if ([persistentID length]) {
+ message[kFIRMessagingMessageIDKey] = persistentID;
+ }
+
+ // third-party data
+ for (GtalkAppData *item in dataMessage.appDataArray) {
+ _FIRMessagingDevAssert(item.hasKey && item.hasValue, @"Invalid AppData");
+
+ // do not process the "from" key -- is not useful
+ if ([kFIRMessagingFromKey isEqualToString:item.key]) {
+ continue;
+ }
+
+ // Filter the "gcm.notification." keys in the message
+ if ([item.key hasPrefix:kMCSNotificationPrefix]) {
+ NSString *key = [item.key substringFromIndex:[kMCSNotificationPrefix length]];
+ if ([key length]) {
+ if (!message[kDataMessageNotificationKey]) {
+ message[kDataMessageNotificationKey] = [NSMutableDictionary dictionary];
+ }
+ message[kDataMessageNotificationKey][key] = item.value;
+ } else {
+ _FIRMessagingDevAssert([key length], @"Invalid key in MCS message: %@", key);
+ FIRMessagingLoggerError(kFIRMessagingMessageCodeDataMessageManager001,
+ @"Invalid key in MCS message: %@", key);
+ }
+ continue;
+ }
+
+ // Filter the "gcm.duplex" key
+ if ([item.key isEqualToString:kFIRMessagingMessageSyncViaMCSKey]) {
+ BOOL value = [item.value boolValue];
+ message[kFIRMessagingMessageSyncViaMCSKey] = @(value);
+ continue;
+ }
+
+ // do not allow keys with "reserved" keyword
+ if ([[item.key lowercaseString] hasPrefix:kMessageReservedPrefix]) {
+ continue;
+ }
+
+ [message setObject:item.value forKey:item.key];
+ }
+ // TODO: Add support for encrypting raw data later
+ return [NSDictionary dictionaryWithDictionary:message];
+}
+
+- (void)didReceiveParsedMessage:(NSDictionary *)message {
+ if ([message[kFCMMessageSpecialMessage] length]) {
+ NSString *messageType = message[kFCMMessageSpecialMessage];
+ if ([kFCMMessageTypeDeletedMessages isEqualToString:messageType]) {
+ // TODO: Maybe trim down message to remove some unnecessary fields.
+ // tell the FCM receiver of deleted messages
+ [self.delegate didDeleteMessagesOnServer];
+ return;
+ }
+ FIRMessagingLoggerError(kFIRMessagingMessageCodeDataMessageManager002,
+ @"Invalid message type received: %@", messageType);
+ } else if (message[kFIRMessagingMessageSyncViaMCSKey]) {
+ // Update SYNC_RMQ with the message
+ BOOL isDuplicate = [self.syncMessageManager didReceiveMCSSyncMessage:message];
+ if (isDuplicate) {
+ return;
+ }
+ }
+ NSString *messageId = message[kFIRMessagingMessageIDKey];
+ NSDictionary *filteredMessage = [self filterInternalFIRMessagingKeysFromMessage:message];
+ [self.delegate didReceiveMessage:filteredMessage withIdentifier:messageId];
+}
+
+- (NSDictionary *)filterInternalFIRMessagingKeysFromMessage:(NSDictionary *)message {
+ NSMutableDictionary *newMessage = [NSMutableDictionary dictionaryWithDictionary:message];
+ for (NSString *key in message) {
+ if ([key hasPrefix:kFIRMessagingMessageInternalReservedKeyword]) {
+ [newMessage removeObjectForKey:key];
+ }
+ }
+ return [newMessage copy];
+}
+
+- (void)sendDataMessageStanza:(NSMutableDictionary *)dataMessage {
+ NSNumber *ttlNumber = dataMessage[kFIRMessagingSendTTL];
+ NSString *to = dataMessage[kFIRMessagingSendTo];
+ NSString *msgId = dataMessage[kFIRMessagingSendMessageID];
+ NSString *appPackage = [self categoryForUpstreamMessages];
+ GtalkDataMessageStanza *stanza = [[GtalkDataMessageStanza alloc] init];
+
+ // TODO: enforce TTL (right now only ttl=0 is special, means no storage)
+ int ttl = [ttlNumber intValue];
+ if (ttl < 0 || ttl > self.ttl) {
+ ttl = self.ttl;
+ }
+ [stanza setTtl:ttl];
+ [stanza setSent:FIRMessagingCurrentTimestampInSeconds()];
+
+ int delay = [self delayForMessage:dataMessage];
+ if (delay > 0) {
+ [stanza setMaxDelay:delay];
+ }
+
+ if (msgId) {
+ [stanza setId_p:msgId];
+ }
+
+ // collapse key as given by the sender
+ NSString *token = dataMessage[KFIRMessagingSendMessageAppData][kFIRMessagingCollapseKey];
+ if ([token length]) {
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeDataMessageManager003,
+ @"FIRMessaging using %@ as collapse key", token);
+ [stanza setToken:token];
+ }
+
+ if (!self.secretToken) {
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeDataMessageManager004,
+ @"Trying to send data message without a secret token. "
+ @"Authentication failed.");
+ [self willSendDataMessageFail:stanza
+ withMessageId:msgId
+ error:kFIRMessagingErrorCodeMissingDeviceID];
+ return;
+ }
+
+ if (![to length]) {
+ [self willSendDataMessageFail:stanza withMessageId:msgId error:kFIRMessagingErrorMissingTo];
+ return;
+ }
+ [stanza setTo:to];
+ [stanza setCategory:appPackage];
+ // required field in the proto this is set by the server
+ // set it to a sentinel so the runtime doesn't throw an exception
+ [stanza setFrom:@""];
+
+ // MCS itself would set the registration ID
+ // [stanza setRegId:nil];
+
+ int size = [self addData:dataMessage[KFIRMessagingSendMessageAppData] toStanza:stanza];
+ if (size > kMaxAppDataSizeDefault) {
+ [self willSendDataMessageFail:stanza withMessageId:msgId error:kFIRMessagingErrorSizeExceeded];
+ return;
+ }
+
+ BOOL useRmq = (ttl != 0) && (msgId != nil);
+ if (useRmq) {
+ if (!self.client.isConnected) {
+ // do nothing assuming rmq save is enabled
+ }
+
+ NSError *error;
+ if (![self.rmq2Manager saveRmqMessage:stanza error:&error]) {
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeDataMessageManager005, @"%@", error);
+ [self willSendDataMessageFail:stanza withMessageId:msgId error:kFIRMessagingErrorSave];
+ return;
+ }
+
+ [self willSendDataMessageSuccess:stanza withMessageId:msgId];
+ }
+
+ // if delay > 0 we don't really care about sending the message right now
+ // so we piggy-back on any other urgent(delay = 0) message that we are sending
+ if (delay > 0 && [self delayMessage:stanza]) {
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeDataMessageManager006, @"Delaying Message %@",
+ dataMessage);
+ return;
+ }
+ // send delayed messages
+ [self sendDelayedMessages:[self.delayedMessagesQueue removeDelayedMessages]];
+
+ BOOL sending = [self tryToSendDataMessageStanza:stanza];
+ if (!sending) {
+ if (useRmq) {
+ NSString *event __unused = [NSString stringWithFormat:@"Queued message: %@", [stanza id_p]];
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeDataMessageManager007, @"%@", event);
+ } else {
+ [self willSendDataMessageFail:stanza
+ withMessageId:msgId
+ error:kFIRMessagingErrorCodeNetwork];
+ return;
+ }
+ }
+}
+
+- (void)sendDelayedMessages:(NSArray *)delayedMessages {
+ for (GtalkDataMessageStanza *message in delayedMessages) {
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeDataMessageManager008,
+ @"%@ Sending delayed message %@", @"DMM", message);
+ [message setActualDelay:(int)(FIRMessagingCurrentTimestampInSeconds() - message.sent)];
+ [self tryToSendDataMessageStanza:message];
+ }
+}
+
+- (void)didSendDataMessageStanza:(GtalkDataMessageStanza *)message {
+ NSString *msgId = [message id_p] ?: @"";
+ [self.delegate didSendDataMessageWithID:msgId];
+}
+
+- (void)addParamWithKey:(NSString *)key
+ value:(NSString *)val
+ toStanza:(GtalkDataMessageStanza *)stanza {
+ if (!key || !val) {
+ return;
+ }
+ GtalkAppData *appData = [[GtalkAppData alloc] init];
+ [appData setKey:key];
+ [appData setValue:val];
+ [[stanza appDataArray] addObject:appData];
+}
+
+/**
+ @return The size of the data being added to stanza.
+ */
+- (int)addData:(NSDictionary *)data toStanza:(GtalkDataMessageStanza *)stanza {
+ int size = 0;
+ for (NSString *key in data) {
+ NSObject *val = data[key];
+ if ([val isKindOfClass:[NSString class]]) {
+ NSString *strVal = (NSString *)val;
+ [self addParamWithKey:key value:strVal toStanza:stanza];
+ size += [key length] + [strVal length];
+ } else if ([val isKindOfClass:[NSNumber class]]) {
+ NSString *strVal = [(NSNumber *)val stringValue];
+ [self addParamWithKey:key value:strVal toStanza:stanza];
+ size += [key length] + [strVal length];
+ } else if ([kFIRMessagingRawDataKey isEqualToString:key] &&
+ [val isKindOfClass:[NSData class]]) {
+ NSData *rawData = (NSData *)val;
+ [stanza setRawData:[rawData copy]];
+ size += [rawData length];
+ } else {
+ FIRMessagingLoggerError(kFIRMessagingMessageCodeDataMessageManager009, @"Ignoring key: %@",
+ key);
+ }
+ }
+ return size;
+}
+
+/**
+ * Notify the messenger that send data message completed with success. This is called for
+ * TTL=0, after the message has been sent, or when message is saved, to unlock the send()
+ * method.
+ */
+- (void)willSendDataMessageSuccess:(GtalkDataMessageStanza *)stanza
+ withMessageId:(NSString *)messageId {
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeDataMessageManager010,
+ @"send message success: %@", messageId);
+ [self.delegate willSendDataMessageWithID:messageId error:nil];
+}
+
+/**
+ * We send 'send failures' from server as normal FIRMessaging messages, with a 'message_type'
+ * extra - same as 'message deleted'.
+ *
+ * For TTL=0 or errors that can be detected during send ( too many messages, invalid, etc)
+ * we throw IOExceptions
+ */
+- (void)willSendDataMessageFail:(GtalkDataMessageStanza *)stanza
+ withMessageId:(NSString *)messageId
+ error:(FIRMessagingInternalErrorCode)errorCode {
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeDataMessageManager011,
+ @"Send message fail: %@ error: %lu", messageId, (unsigned long)errorCode);
+
+ NSError *error = [NSError errorWithFCMErrorCode:errorCode];
+ if ([self.delegate respondsToSelector:@selector(willSendDataMessageWithID:error:)]) {
+ [self.delegate willSendDataMessageWithID:messageId error:error];
+ }
+}
+
+- (void)resendMessagesWithConnection:(FIRMessagingConnection *)connection {
+ NSMutableString *rmqIdsResent = [NSMutableString string];
+ NSMutableArray *toRemoveRmqIds = [NSMutableArray array];
+ FIRMessaging_WEAKIFY(self);
+ FIRMessaging_WEAKIFY(connection);
+ FIRMessagingRmqMessageHandler messageHandler = ^(int64_t rmqId, int8_t tag, NSData *data) {
+ FIRMessaging_STRONGIFY(self);
+ FIRMessaging_STRONGIFY(connection);
+ GPBMessage *proto =
+ [FIRMessagingGetClassForTag((FIRMessagingProtoTag)tag) parseFromData:data error:NULL];
+ if ([proto isKindOfClass:GtalkDataMessageStanza.class]) {
+ GtalkDataMessageStanza *stanza = (GtalkDataMessageStanza *)proto;
+
+ if (![self handleExpirationForDataMessage:stanza]) {
+ // time expired let's delete from RMQ
+ [toRemoveRmqIds addObject:stanza.persistentId];
+ return;
+ }
+ [rmqIdsResent appendString:[NSString stringWithFormat:@"%@,", stanza.id_p]];
+ }
+
+ [connection sendProto:proto];
+ };
+ [self.rmq2Manager scanWithRmqMessageHandler:messageHandler
+ dataMessageHandler:nil];
+
+ if ([rmqIdsResent length]) {
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeDataMessageManager012, @"Resent: %@",
+ rmqIdsResent);
+ }
+
+ if ([toRemoveRmqIds count]) {
+ [self.rmq2Manager removeRmqMessagesWithRmqIds:toRemoveRmqIds];
+ }
+}
+
+/**
+ * Check the TTL and generate an error if needed.
+ *
+ * @return false if the message needs to be deleted
+ */
+- (BOOL)handleExpirationForDataMessage:(GtalkDataMessageStanza *)message {
+ if (message.ttl == 0) {
+ return NO;
+ }
+
+ int64_t now = FIRMessagingCurrentTimestampInSeconds();
+ if (now > message.sent + message.ttl) {
+ [self willSendDataMessageFail:message
+ withMessageId:message.id_p
+ error:kFIRMessagingErrorServiceNotAvailable];
+ return NO;
+ }
+ return YES;
+}
+
+#pragma mark - Private
+
+- (int)delayForMessage:(NSMutableDictionary *)message {
+ int delay = 0; // default
+ if (message[kFIRMessagingSendDelay]) {
+ delay = [message[kFIRMessagingSendDelay] intValue];
+ [message removeObjectForKey:kFIRMessagingSendDelay];
+ if (delay < kMinDelaySeconds) {
+ delay = 0;
+ } else if (delay > kMaxDelaySeconds) {
+ delay = kMaxDelaySeconds;
+ }
+ }
+ return delay;
+}
+
+// return True if successfully delayed else False
+- (BOOL)delayMessage:(GtalkDataMessageStanza *)message {
+ return [self.delayedMessagesQueue queueMessage:message];
+}
+
+- (BOOL)tryToSendDataMessageStanza:(GtalkDataMessageStanza *)stanza {
+ if (self.client.isConnectionActive) {
+ [self.client sendMessage:stanza];
+ return YES;
+ }
+
+ // if we only reconnect for TTL = 0 messages check if we ttl = 0 or
+ // if we reconnect for all messages try to reconnect
+ if ((self.upstreamForceReconnect == kUpstreamForceReconnectTTL0 && stanza.ttl == 0) ||
+ self.upstreamForceReconnect == kUpstreamForceReconnectAll) {
+ BOOL isNetworkAvailable = [[FIRMessaging messaging] isNetworkAvailable];
+ if (isNetworkAvailable) {
+ if (stanza.ttl == 0) {
+ // Add TTL = 0 messages to be sent on next connect. TTL != 0 messages are
+ // persisted, and will be sent from the RMQ.
+ [self.client sendOnConnectOrDrop:stanza];
+ }
+
+ [self.client retryConnectionImmediately:YES];
+ return YES;
+ }
+ }
+ return NO;
+}
+
+- (NSString *)categoryForUpstreamMessages {
+ return FIRMessagingAppIdentifier();
+}
+
+@end
diff --git a/Firebase/Messaging/FIRMessagingDefines.h b/Firebase/Messaging/FIRMessagingDefines.h
new file mode 100644
index 0000000..36448ed
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingDefines.h
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef FIRMessaging_xcodeproj_FIRMessagingDefines_h
+#define FIRMessaging_xcodeproj_FIRMessagingDefines_h
+
+#define _FIRMessaging_VERBOSE_LOGGING 1
+
+// Verbose Logging
+#if (_FIRMessaging_VERBOSE_LOGGING)
+#define FIRMessaging_DEV_VERBOSE_LOG(...) NSLog(__VA_ARGS__)
+#else
+#define FIRMessaging_DEV_VERBOSE_LOG(...) do { } while (0)
+#endif // FIRMessaging_VERBOSE_LOGGING
+
+
+// FIRMessaging_FAIL
+#ifdef DEBUG
+#define FIRMessaging_FAIL(format, ...) \
+do { \
+ NSLog(format, ##__VA_ARGS__); \
+ __builtin_trap(); \
+} while (false)
+#else
+#define FIRMessaging_FAIL(...) do { } while (0)
+#endif
+
+
+// WEAKIFY & STRONGIFY
+// Helper macro.
+#define _FIRMessaging_WEAKNAME(VAR) VAR ## _weak_
+
+#define FIRMessaging_WEAKIFY(VAR) __weak __typeof__(VAR) _FIRMessaging_WEAKNAME(VAR) = (VAR);
+
+#define FIRMessaging_STRONGIFY(VAR) \
+_Pragma("clang diagnostic push") \
+_Pragma("clang diagnostic ignored \"-Wshadow\"") \
+__strong __typeof__(VAR) VAR = _FIRMessaging_WEAKNAME(VAR); \
+_Pragma("clang diagnostic pop")
+
+
+// Type Conversions (used for NSInteger etc)
+#ifndef _FIRMessaging_L
+#define _FIRMessaging_L(v) (long)(v)
+#endif
+
+#ifndef _FIRMessaging_UL
+#define _FIRMessaging_UL(v) (unsigned long)(v)
+#endif
+
+#endif
+
+// Debug Assert
+#ifndef _FIRMessagingDevAssert
+// we directly invoke the NSAssert handler so we can pass on the varargs
+// (NSAssert doesn't have a macro we can use that takes varargs)
+#if !defined(NS_BLOCK_ASSERTIONS)
+#define _FIRMessagingDevAssert(condition, ...) \
+ do { \
+ if (!(condition)) { \
+ [[NSAssertionHandler currentHandler] \
+ handleFailureInFunction:(NSString *) \
+ [NSString stringWithUTF8String:__PRETTY_FUNCTION__] \
+ file:(NSString *)[NSString stringWithUTF8String:__FILE__] \
+ lineNumber:__LINE__ \
+ description:__VA_ARGS__]; \
+ } \
+ } while(0)
+#else // !defined(NS_BLOCK_ASSERTIONS)
+#define _FIRMessagingDevAssert(condition, ...) do { } while (0)
+#endif // !defined(NS_BLOCK_ASSERTIONS)
+
+#endif // _FIRMessagingDevAssert
+
+// Invalidates the initializer from which it's called.
+#ifndef FIRMessagingInvalidateInitializer
+#define FIRMessagingInvalidateInitializer() \
+ do { \
+ [self class]; /* Avoid warning of dead store to |self|. */ \
+ _FIRMessagingDevAssert(NO, @"Invalid initializer."); \
+ return nil; \
+ } while (0)
+#endif
diff --git a/Firebase/Messaging/FIRMessagingDelayedMessageQueue.h b/Firebase/Messaging/FIRMessagingDelayedMessageQueue.h
new file mode 100644
index 0000000..d20ec91
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingDelayedMessageQueue.h
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+@class GtalkDataMessageStanza;
+@class FIRMessagingRmqManager;
+
+@protocol FIRMessagingRmqScanner;
+
+typedef void(^FIRMessagingSendDelayedMessagesHandler)(NSArray *messages);
+
+@interface FIRMessagingDelayedMessageQueue : NSObject
+
+- (instancetype)initWithRmqScanner:(id<FIRMessagingRmqScanner>)rmqScanner
+ sendDelayedMessagesHandler:(FIRMessagingSendDelayedMessagesHandler)sendDelayedMessagesHandler;
+
+- (BOOL)queueMessage:(GtalkDataMessageStanza *)message;
+
+- (NSArray *)removeDelayedMessages;
+
+@end
diff --git a/Firebase/Messaging/FIRMessagingDelayedMessageQueue.m b/Firebase/Messaging/FIRMessagingDelayedMessageQueue.m
new file mode 100644
index 0000000..0371c02
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingDelayedMessageQueue.m
@@ -0,0 +1,146 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRMessagingDelayedMessageQueue.h"
+
+#import "Protos/GtalkCore.pbobjc.h"
+
+#import "FIRMessagingDefines.h"
+#import "FIRMessagingRmqManager.h"
+#import "FIRMessagingUtilities.h"
+
+static const int kMaxQueuedMessageCount = 10;
+
+@interface FIRMessagingDelayedMessageQueue ()
+
+@property(nonatomic, readonly, weak) id<FIRMessagingRmqScanner> rmqScanner;
+@property(nonatomic, readonly, copy) FIRMessagingSendDelayedMessagesHandler sendDelayedMessagesHandler;
+
+@property(nonatomic, readwrite, assign) int persistedMessageCount;
+// the scheduled timeout or -1 if not set
+@property(nonatomic, readwrite, assign) int64_t scheduledTimeoutMilliseconds;
+// The time of the last scan of the message DB,
+// used to avoid retrieving messages more than once.
+@property(nonatomic, readwrite, assign) int64_t lastDBScanTimestampSeconds;
+
+@property(nonatomic, readwrite, strong) NSMutableArray *messages;
+@property(nonatomic, readwrite, strong) NSTimer *sendTimer;
+
+@end
+
+@implementation FIRMessagingDelayedMessageQueue
+
+- (instancetype)init {
+ FIRMessagingInvalidateInitializer();
+}
+
+- (instancetype)initWithRmqScanner:(id<FIRMessagingRmqScanner>)rmqScanner
+ sendDelayedMessagesHandler:(FIRMessagingSendDelayedMessagesHandler)sendDelayedMessagesHandler {
+ _FIRMessagingDevAssert(sendDelayedMessagesHandler, @"Invalid nil callback for delayed messages");
+ self = [super init];
+ if (self) {
+ _rmqScanner = rmqScanner;
+ _sendDelayedMessagesHandler = sendDelayedMessagesHandler;
+ _messages = [NSMutableArray arrayWithCapacity:10];
+ _scheduledTimeoutMilliseconds = -1;
+ }
+ return self;
+}
+
+- (BOOL)queueMessage:(GtalkDataMessageStanza *)message {
+ if (self.messages.count >= kMaxQueuedMessageCount) {
+ return NO;
+ }
+ if (message.ttl == 0) {
+ // ttl=0 messages aren't persisted, add it to memory
+ [self.messages addObject:message];
+ } else {
+ self.persistedMessageCount++;
+ }
+ int64_t timeoutMillis = [self calculateTimeoutInMillisWithDelayInSeconds:message.maxDelay];
+ if (![self isTimeoutScheduled] || timeoutMillis < self.scheduledTimeoutMilliseconds) {
+ [self scheduleTimeoutInMillis:timeoutMillis];
+ }
+ return YES;
+}
+
+- (NSArray *)removeDelayedMessages {
+ [self cancelTimeout];
+ if ([self messageCount] == 0) {
+ return @[];
+ }
+
+ NSMutableArray *delayedMessages = [NSMutableArray array];
+ // add the ttl=0 messages
+ if (self.messages.count) {
+ [delayedMessages addObjectsFromArray:delayedMessages];
+ [self.messages removeAllObjects];
+ }
+
+ // add persistent messages
+ if (self.persistedMessageCount > 0) {
+ FIRMessaging_WEAKIFY(self);
+ [self.rmqScanner scanWithRmqMessageHandler:nil
+ dataMessageHandler:^(int64_t rmqId, GtalkDataMessageStanza *stanza) {
+ FIRMessaging_STRONGIFY(self);
+ if ([stanza hasMaxDelay] &&
+ [stanza sent] >= self.lastDBScanTimestampSeconds) {
+ [delayedMessages addObject:stanza];
+ }
+ }];
+ self.lastDBScanTimestampSeconds = FIRMessagingCurrentTimestampInSeconds();
+ self.persistedMessageCount = 0;
+ }
+ return delayedMessages;
+}
+
+- (void)sendMessages {
+ if (self.sendDelayedMessagesHandler) {
+ self.sendDelayedMessagesHandler([self removeDelayedMessages]);
+ }
+}
+
+#pragma mark - Private
+
+- (NSInteger)messageCount {
+ return self.messages.count + self.persistedMessageCount;
+}
+
+- (BOOL)isTimeoutScheduled {
+ return self.scheduledTimeoutMilliseconds > 0;
+}
+
+- (int64_t)calculateTimeoutInMillisWithDelayInSeconds:(int)delay {
+ return FIRMessagingCurrentTimestampInMilliseconds() + delay * 1000.0;
+}
+
+- (void)scheduleTimeoutInMillis:(int64_t)time {
+ [self cancelTimeout];
+ self.scheduledTimeoutMilliseconds = time;
+ double delay = (time - FIRMessagingCurrentTimestampInMilliseconds()) / 1000.0;
+ [self performSelector:@selector(sendMessages) withObject:self afterDelay:delay];
+}
+
+- (void)cancelTimeout {
+ if ([self isTimeoutScheduled]) {
+ [NSObject cancelPreviousPerformRequestsWithTarget:self
+ selector:@selector(sendMessages)
+ object:nil];
+ self.scheduledTimeoutMilliseconds = -1;
+ }
+}
+
+@end
diff --git a/Firebase/Messaging/FIRMessagingFileLogger.h b/Firebase/Messaging/FIRMessagingFileLogger.h
new file mode 100644
index 0000000..ec11369
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingFileLogger.h
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRMessagingLogger.h"
+
+#if FIRMessaging_PROBER
+@interface FIRMessagingFileLogFilter : NSObject <FIRMessagingLogFilter>
+
+@end
+
+@interface FIRMessagingFileLogFormatter : NSObject <FIRMessagingLogFormatter>
+
+@end
+
+@interface FIRMessagingFileLogWriter : NSObject <FIRMessagingLogWriter>
+
+@end
+#endif
diff --git a/Firebase/Messaging/FIRMessagingFileLogger.m b/Firebase/Messaging/FIRMessagingFileLogger.m
new file mode 100644
index 0000000..7570a79
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingFileLogger.m
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRMessagingFileLogger.h"
+
+#if FIRMessaging_PROBER
+
+#import "DDFileLogger.h"
+#import "DDLog.h"
+
+@interface FIRMessagingFileLogFilter ()
+
+@property(nonatomic, readwrite, assign) FIRMessagingLogLevel level;
+@end
+
+@implementation FIRMessagingFileLogFilter
+
+#pragma mark - GTMLogFilter protocol
+
+- (BOOL)filterAllowsMessage:(NSString *)msg level:(FIRMessagingLogLevel)level {
+ // allow everything
+ return YES;
+}
+
+@end
+
+@interface FIRMessagingFileLogFormatter ()
+
+@property(nonatomic, readwrite, strong) NSDateFormatter *dateFormatter;
+
+@end
+
+@implementation FIRMessagingFileLogFormatter
+
+static NSString *const kFIRMessagingLogPrefix = @"FIRMessaging";
+
+- (id)init {
+ if ((self = [super init])) {
+ _dateFormatter = [[NSDateFormatter alloc] init];
+ [_dateFormatter setFormatterBehavior:NSDateFormatterBehavior10_4];
+ [_dateFormatter setDateFormat:@"yyyy-MM-dd HH:mm:ss.SSS"];
+ }
+ return self;
+}
+
+#pragma mark - GTMLogFormatter protocol
+
+static DDLogMessage *currentMessage;
+- (NSString *)stringForFunc:(NSString *)func
+ withFormat:(NSString *)fmt
+ valist:(va_list)args
+ level:(FIRMessagingLogLevel)level {
+ NSString *logMessage = [[NSString alloc] initWithFormat:fmt arguments:args];
+ currentMessage = [[DDLogMessage alloc] initWithMessage:logMessage
+ level:0
+ flag:0
+ context:0
+ file:NULL
+ function:NULL
+ line:0
+ tag:0
+ options:0
+ timestamp:[NSDate date]];
+ return logMessage;
+}
+
+@end
+
+@interface FIRMessagingFileLogWriter ()
+
+@property(nonatomic, readwrite, strong) DDFileLogger *fileLogger;
+
+@end
+
+@implementation FIRMessagingFileLogWriter
+
+- (instancetype)init {
+ self = [super init];
+ if (self) {
+ _fileLogger = [[DDFileLogger alloc] init];
+ }
+ return self;
+}
+
+#pragma mark - GTMLogWriter protocol
+
+- (void)logMessage:(NSString *)msg level:(FIRMessagingLogLevel)level {
+ // log to stdout
+ NSLog(@"%@", msg);
+ [self.fileLogger logMessage:currentMessage];
+}
+
+@end
+
+#endif
diff --git a/Firebase/Messaging/FIRMessagingInstanceIDProxy.h b/Firebase/Messaging/FIRMessagingInstanceIDProxy.h
new file mode 100644
index 0000000..b7ebd4b
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingInstanceIDProxy.h
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+typedef void(^FIRMessagingInstanceIDProxyTokenHandler)(NSString * __nullable token,
+ NSError * __nullable error);
+
+typedef void(^FIRMessagingInstanceIDProxyDeleteTokenHandler)(NSError * __nullable error);
+
+typedef NS_ENUM(NSInteger, FIRMessagingInstanceIDProxyAPNSTokenType) {
+ /// Unknown token type.
+ FIRMessagingInstanceIDProxyAPNSTokenTypeUnknown,
+ /// Sandbox token type.
+ FIRMessagingInstanceIDProxyAPNSTokenTypeSandbox,
+ /// Production token type.
+ FIRMessagingInstanceIDProxyAPNSTokenTypeProd,
+};
+
+/**
+ * FIRMessaging cannot always depend on FIRInstanceID directly, due to how FIRMessaging is
+ * packaged. To make it easier to make calls to FIRInstanceID, this proxy class, will provide
+ * method names duplicated from FIRInstanceID, while using reflection-based called to proxy
+ * the requests.
+ */
+@interface FIRMessagingInstanceIDProxy : NSObject
+
+- (void)setAPNSToken:(nonnull NSData *)token type:(FIRMessagingInstanceIDProxyAPNSTokenType)type;
+
+#pragma mark - Tokens
+
+- (nullable NSString *)token;
+
+- (void)tokenWithAuthorizedEntity:(nonnull NSString *)authorizedEntity
+ scope:(nonnull NSString *)scope
+ options:(nullable NSDictionary *)options
+ handler:(nonnull FIRMessagingInstanceIDProxyTokenHandler)handler;
+
+- (void)deleteTokenWithAuthorizedEntity:(nonnull NSString *)authorizedEntity
+ scope:(nonnull NSString *)scope
+ handler:
+ (nonnull FIRMessagingInstanceIDProxyDeleteTokenHandler)handler;
+@end
diff --git a/Firebase/Messaging/FIRMessagingInstanceIDProxy.m b/Firebase/Messaging/FIRMessagingInstanceIDProxy.m
new file mode 100644
index 0000000..01b4e73
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingInstanceIDProxy.m
@@ -0,0 +1,123 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRMessagingInstanceIDProxy.h"
+
+@implementation FIRMessagingInstanceIDProxy
+
++ (nonnull instancetype)instanceIDProxy {
+ static id proxyInstanceID = nil;
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ Class instanceIDClass = NSClassFromString(@"FIRInstanceID");
+ if (!instanceIDClass) {
+ proxyInstanceID = nil;
+ return;
+ }
+ SEL instanceIDSelector = NSSelectorFromString(@"instanceID");
+ if (![instanceIDClass respondsToSelector:instanceIDSelector]) {
+ proxyInstanceID = nil;
+ return;
+ }
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
+ proxyInstanceID = [instanceIDClass performSelector:instanceIDSelector];
+#pragma clang diagnostic pop
+ });
+ return (FIRMessagingInstanceIDProxy *)proxyInstanceID;
+
+}
+
+- (void)setAPNSToken:(nonnull NSData *)token
+ type:(FIRMessagingInstanceIDProxyAPNSTokenType)type {
+ id proxy = [[self class] instanceIDProxy];
+
+ SEL setAPNSTokenSelector = NSSelectorFromString(@"setAPNSToken:type:");
+ if (![proxy respondsToSelector:setAPNSTokenSelector]) {
+ return;
+ }
+ // Since setAPNSToken takes a scalar value, use NSInvocation
+ NSMethodSignature *methodSignature =
+ [[proxy class] instanceMethodSignatureForSelector:setAPNSTokenSelector];
+ NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature];
+ invocation.selector = setAPNSTokenSelector;
+ invocation.target = proxy;
+ [invocation setArgument:&token atIndex:2];
+ [invocation setArgument:&type atIndex:3];
+ [invocation invoke];
+}
+
+#pragma mark - Tokens
+
+- (nullable NSString *)token {
+ id proxy = [[self class] instanceIDProxy];
+ SEL getTokenSelector = NSSelectorFromString(@"token");
+ if (![proxy respondsToSelector:getTokenSelector]) {
+ return nil;
+ }
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
+ return [proxy performSelector:getTokenSelector];
+#pragma clang diagnostic pop
+}
+
+
+- (void)tokenWithAuthorizedEntity:(nonnull NSString *)authorizedEntity
+ scope:(nonnull NSString *)scope
+ options:(nullable NSDictionary *)options
+ handler:(nonnull FIRMessagingInstanceIDProxyTokenHandler)handler {
+
+ id proxy = [[self class] instanceIDProxy];
+ SEL getTokenSelector = NSSelectorFromString(@"tokenWithAuthorizedEntity:scope:options:handler:");
+ if (![proxy respondsToSelector:getTokenSelector]) {
+ return;
+ }
+ // Since there are >2 arguments, use NSInvocation
+ NSMethodSignature *methodSignature =
+ [[proxy class] instanceMethodSignatureForSelector:getTokenSelector];
+ NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature];
+ invocation.selector = getTokenSelector;
+ invocation.target = proxy;
+ [invocation setArgument:&authorizedEntity atIndex:2];
+ [invocation setArgument:&scope atIndex:3];
+ [invocation setArgument:&options atIndex:4];
+ [invocation setArgument:&handler atIndex:5];
+ [invocation invoke];
+}
+
+- (void)deleteTokenWithAuthorizedEntity:(nonnull NSString *)authorizedEntity
+ scope:(nonnull NSString *)scope
+ handler:
+ (nonnull FIRMessagingInstanceIDProxyDeleteTokenHandler)handler {
+
+ id proxy = [[self class] instanceIDProxy];
+ SEL deleteTokenSelector = NSSelectorFromString(@"deleteTokenWithAuthorizedEntity:scope:handler:");
+ if (![proxy respondsToSelector:deleteTokenSelector]) {
+ return;
+ }
+ // Since there are >2 arguments, use NSInvocation
+ NSMethodSignature *methodSignature =
+ [[proxy class] instanceMethodSignatureForSelector:deleteTokenSelector];
+ NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature];
+ invocation.selector = deleteTokenSelector;
+ invocation.target = proxy;
+ [invocation setArgument:&authorizedEntity atIndex:2];
+ [invocation setArgument:&scope atIndex:3];
+ [invocation setArgument:&handler atIndex:4];
+ [invocation invoke];
+}
+
+@end
diff --git a/Firebase/Messaging/FIRMessagingLogger.h b/Firebase/Messaging/FIRMessagingLogger.h
new file mode 100644
index 0000000..cd3c29a
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingLogger.h
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRMessagingConfig.h"
+#import "FIRMMessageCode.h"
+
+// The convenience macros are only defined if they haven't already been defined.
+#ifndef FIRMessagingLoggerInfo
+
+// Convenience macros that log to the shared FIRMessagingLogger instance. These macros
+// are how users should typically log to FIRMessagingLogger.
+#define FIRMessagingLoggerDebug(code, ...) \
+ [FIRMessagingSharedLogger() logFuncDebug:__func__ messageCode:code msg:__VA_ARGS__]
+#define FIRMessagingLoggerInfo(code, ...) \
+ [FIRMessagingSharedLogger() logFuncInfo:__func__ messageCode:code msg:__VA_ARGS__]
+#define FIRMessagingLoggerNotice(code, ...) \
+ [FIRMessagingSharedLogger() logFuncNotice:__func__ messageCode:code msg:__VA_ARGS__]
+#define FIRMessagingLoggerWarn(code, ...) \
+ [FIRMessagingSharedLogger() logFuncWarning:__func__ messageCode:code msg:__VA_ARGS__]
+#define FIRMessagingLoggerError(code, ...) \
+ [FIRMessagingSharedLogger() logFuncError:__func__ messageCode:code msg:__VA_ARGS__]
+
+#endif // !defined(FIRMessagingLoggerInfo)
+
+/// Protocols
+@protocol FIRMessagingLogFormatter <NSObject>
+- (NSString *)stringForFunc:(NSString *)func
+ withFormat:(NSString *)fmt
+ valist:(va_list)args
+ level:(FIRMessagingLogLevel)level NS_FORMAT_FUNCTION(2, 0);
+@end
+
+/// FIRMessagingLogWriter
+@protocol FIRMessagingLogWriter <NSObject>
+// Writes the given log message to where the log writer is configured to write.
+- (void)logMessage:(NSString *)msg level:(FIRMessagingLogLevel)level;
+@end
+
+/// FIRMessagingLogFilter
+@protocol FIRMessagingLogFilter <NSObject>
+// Returns YES if |msg| at |level| should be logged; NO otherwise.
+- (BOOL)filterAllowsMessage:(NSString *)msg level:(FIRMessagingLogLevel)level;
+@end
+
+@interface FIRMessagingLogLevelFilter : NSObject <FIRMessagingLogFilter>
+- (instancetype)initWithLevel:(FIRMessagingLogLevel)level;
+@end
+
+
+@interface FIRMessagingLogger : NSObject
+
+@property(nonatomic, readwrite, strong) id<FIRMessagingLogFilter> filter;
+@property(nonatomic, readwrite, strong) id<FIRMessagingLogWriter> writer;
+@property(nonatomic, readwrite, strong) id<FIRMessagingLogFormatter> formatter;
+
+- (void)logFuncDebug:(const char *)func
+ messageCode:(FIRMessagingMessageCode)messageCode
+ msg:(NSString *)fmt, ... NS_FORMAT_FUNCTION(3, 4);
+
+- (void)logFuncInfo:(const char *)func
+ messageCode:(FIRMessagingMessageCode)messageCode
+ msg:(NSString *)fmt, ... NS_FORMAT_FUNCTION(3, 4);
+
+- (void)logFuncNotice:(const char *)func
+ messageCode:(FIRMessagingMessageCode)messageCode
+ msg:(NSString *)fmt, ... NS_FORMAT_FUNCTION(3, 4);
+
+- (void)logFuncWarning:(const char *)func
+ messageCode:(FIRMessagingMessageCode)messageCode
+ msg:(NSString *)fmt, ... NS_FORMAT_FUNCTION(3, 4);
+
+- (void)logFuncError:(const char *)func
+ messageCode:(FIRMessagingMessageCode)messageCode
+ msg:(NSString *)fmt, ... NS_FORMAT_FUNCTION(3, 4);
+
+@end
+
+/**
+ * Instantiates and/or returns a shared FIRMessagingLogger used exclusively
+ * for FIRMessaging log messages.
+ *
+ * @return the shared FIRMessagingLogger instance
+ */
+FIRMessagingLogger *FIRMessagingSharedLogger();
diff --git a/Firebase/Messaging/FIRMessagingLogger.m b/Firebase/Messaging/FIRMessagingLogger.m
new file mode 100644
index 0000000..0ded97c
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingLogger.m
@@ -0,0 +1,305 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRMessagingLogger.h"
+
+#import "FIRLogger.h"
+#import "FIRMessagingFileLogger.h"
+
+/**
+ * A log formatter that prefixes log messages with "FIRMessaging".
+ */
+@interface FIRMessagingLogStandardFormatter : NSObject<FIRMessagingLogFormatter>
+
+@property(nonatomic, readwrite, strong) NSDateFormatter *dateFormatter;
+
+@end
+
+@implementation FIRMessagingLogStandardFormatter
+
+static NSString *const kFIRMessagingLogPrefix = @"FIRMessaging";
+
+- (id)init {
+ if ((self = [super init])) {
+ _dateFormatter = [[NSDateFormatter alloc] init];
+ [_dateFormatter setFormatterBehavior:NSDateFormatterBehavior10_4];
+ [_dateFormatter setDateFormat:@"yyyy-MM-dd HH:mm:ss.SSS"];
+ }
+ return self;
+}
+/**
+ * Returns a formatted string prefixed with "FIRMessaging" to allow
+ * FIRMessaging output to be easily differentiated in logs.
+ *
+ * @param func the name of the function calling the logger
+ * @param fmt the format string
+ * @param args the list of arguments for the format string
+ * @param level the logging level (eg. debug, info)
+ * @return the formatted string prefixed with "FIRMessaging".
+ */
+- (NSString *)stringForFunc:(NSString *)func
+ withFormat:(NSString *)fmt
+ valist:(va_list)args
+ level:(FIRMessagingLogLevel)level NS_FORMAT_FUNCTION(2, 0) {
+ if (!(fmt && args)) {
+ return nil;
+ }
+
+ NSString *logMessage = [[NSString alloc] initWithFormat:fmt arguments:args];
+ NSString *logLevelString = [self stringForLogLevel:level];
+ NSString *dateString = [self.dateFormatter stringFromDate:[NSDate date]];
+ return [NSString stringWithFormat:@"%@: <%@/%@> %@",
+ dateString, kFIRMessagingLogPrefix, logLevelString, logMessage];
+}
+
+- (NSString *)stringForLogLevel:(FIRMessagingLogLevel)level {
+ switch (level) {
+ case kFIRMessagingLogLevelDebug:
+ return @"DEBUG";
+
+ case kFIRMessagingLogLevelInfo:
+ return @"INFO";
+
+ case kFIRMessagingLogLevelError:
+ return @"WARNING";
+
+ case kFIRMessagingLogLevelAssert:
+ return @"ERROR";
+
+ default:
+ return @"INFO";
+ }
+}
+
+@end
+
+@interface FIRMessagingLogLevelFilter ()
+
+@property(nonatomic, readwrite, assign) FIRMessagingLogLevel level;
+
+@end
+
+@implementation FIRMessagingLogLevelFilter
+
+- (instancetype)initWithLevel:(FIRMessagingLogLevel)level {
+ self = [super init];
+ if (self) {
+ _level = level;
+ }
+ return self;
+}
+
+- (BOOL)filterAllowsMessage:(NSString *)msg level:(FIRMessagingLogLevel)level {
+#if defined(DEBUG) && DEBUG
+ return YES;
+#endif
+
+ BOOL allow = YES;
+
+ switch (level) {
+ case kFIRMessagingLogLevelDebug:
+ allow = NO;
+ break;
+ case kFIRMessagingLogLevelInfo:
+ case kFIRMessagingLogLevelError:
+ case kFIRMessagingLogLevelAssert:
+ allow = (level >= self.level);
+ break;
+ default:
+ allow = NO;
+ break;
+ }
+
+ return allow;
+}
+
+@end
+
+
+// Copied from FIRMessagingLogger. Standard implementation to write logs to console.
+@interface NSFileHandle (FIRMessagingFileHandleLogWriter) <FIRMessagingLogWriter>
+@end
+
+@implementation NSFileHandle (FIRMessagingFileHandleLogWriter)
+- (void)logMessage:(NSString *)msg level:(FIRMessagingLogLevel)level {
+ @synchronized(self) {
+ // Closed pipes should not generate exceptions in our caller. Catch here
+ // as well [FIRMessagingLogger logInternalFunc:...] so that an exception in this
+ // writer does not prevent other writers from having a chance.
+ @try {
+ NSString *line = [NSString stringWithFormat:@"%@\n", msg];
+ [self writeData:[line dataUsingEncoding:NSUTF8StringEncoding]];
+ }
+ @catch (id e) {
+ // Ignored
+ }
+ }
+}
+@end
+
+@interface FIRMessagingLogger ()
+
+@end
+
+@implementation FIRMessagingLogger
+
++ (instancetype)standardLogger {
+
+ id<FIRMessagingLogWriter> writer;
+ id<FIRMessagingLogFormatter> formatter;
+ id<FIRMessagingLogFilter> filter;
+
+#if FIRMessaging_PROBER
+ writer = [[FIRMessagingFileLogWriter alloc] init];
+ formatter = [[FIRMessagingFileLogFormatter alloc] init];
+ filter = [[FIRMessagingFileLogFilter alloc] init];
+#else
+ writer = [NSFileHandle fileHandleWithStandardOutput];
+ formatter = [[FIRMessagingLogStandardFormatter alloc] init];
+ filter = [[FIRMessagingLogLevelFilter alloc] init];
+#endif
+
+ return [[FIRMessagingLogger alloc] initWithFilter:filter formatter:formatter writer:writer];
+}
+
+- (instancetype)initWithFilter:(id<FIRMessagingLogFilter>)filter
+ formatter:(id<FIRMessagingLogFormatter>)formatter
+ writer:(id<FIRMessagingLogWriter>)writer {
+ self = [super init];
+ if (self) {
+ _filter = filter;
+ _formatter = formatter;
+ _writer = writer;
+ }
+ return self;
+}
+
+#pragma mark - Log Helpers
+
++ (NSString *)formatMessageCode:(FIRMessagingMessageCode)messageCode {
+ return [NSString stringWithFormat:@"I-FCM%06ld", (long)messageCode];
+}
+
+- (void)logFuncDebug:(const char *)func
+ messageCode:(FIRMessagingMessageCode)messageCode
+ msg:(NSString *)fmt, ... {
+ va_list args;
+ va_start(args, fmt);
+ FIRLogBasic(FIRLoggerLevelDebug, kFIRLoggerMessaging,
+ [FIRMessagingLogger formatMessageCode:messageCode], fmt, args);
+ va_end(args);
+#if FIRMessaging_PROBER
+ va_start(args, fmt);
+ [self logInternalFunc:func format:fmt valist:args level:kFIRMessagingLogLevelDebug];
+ va_end(args);
+#endif
+}
+
+- (void)logFuncInfo:(const char *)func
+ messageCode:(FIRMessagingMessageCode)messageCode
+ msg:(NSString *)fmt, ... {
+ va_list args;
+ va_start(args, fmt);
+ FIRLogBasic(FIRLoggerLevelInfo, kFIRLoggerMessaging,
+ [FIRMessagingLogger formatMessageCode:messageCode], fmt, args);
+ va_end(args);
+#if FIRMessaging_PROBER
+ va_start(args, fmt);
+ [self logInternalFunc:func format:fmt valist:args level:kFIRMessagingLogLevelInfo];
+ va_end(args);
+#endif
+}
+
+- (void)logFuncNotice:(const char *)func
+ messageCode:(FIRMessagingMessageCode)messageCode
+ msg:(NSString *)fmt, ... {
+ va_list args;
+ va_start(args, fmt);
+ FIRLogBasic(FIRLoggerLevelNotice, kFIRLoggerMessaging,
+ [FIRMessagingLogger formatMessageCode:messageCode], fmt, args);
+ va_end(args);
+#if FIRMessaging_PROBER
+ va_start(args, fmt);
+ // Treat FIRLoggerLevelNotice as "info" locally, since we don't have an equivalent
+ [self logInternalFunc:func format:fmt valist:args level:kFIRMessagingLogLevelInfo];
+ va_end(args);
+#endif
+}
+
+- (void)logFuncWarning:(const char *)func
+ messageCode:(FIRMessagingMessageCode)messageCode
+ msg:(NSString *)fmt, ... {
+ va_list args;
+ va_start(args, fmt);
+ FIRLogBasic(FIRLoggerLevelWarning, kFIRLoggerMessaging,
+ [FIRMessagingLogger formatMessageCode:messageCode], fmt, args);
+ va_end(args);
+#if FIRMessaging_PROBER
+ va_start(args, fmt);
+ // Treat FIRLoggerLevelWarning as "error" locally, since we don't have an equivalent
+ [self logInternalFunc:func format:fmt valist:args level:kFIRMessagingLogLevelError];
+ va_end(args);
+#endif
+}
+
+- (void)logFuncError:(const char *)func
+ messageCode:(FIRMessagingMessageCode)messageCode
+ msg:(NSString *)fmt, ... {
+ va_list args;
+ va_start(args, fmt);
+ FIRLogBasic(FIRLoggerLevelError, kFIRLoggerMessaging,
+ [FIRMessagingLogger formatMessageCode:messageCode], fmt, args);
+ va_end(args);
+#if FIRMessaging_PROBER
+ va_start(args, fmt);
+ [self logInternalFunc:func format:fmt valist:args level:kFIRMessagingLogLevelError];
+ va_end(args);
+#endif
+}
+
+#pragma mark - Internal Helpers
+
+- (void)logInternalFunc:(const char *)func
+ format:(NSString *)fmt
+ valist:(va_list)args
+ level:(FIRMessagingLogLevel)level {
+ // Primary point where logging happens, logging should never throw, catch
+ // everything.
+ @try {
+ NSString *fname = func ? [NSString stringWithUTF8String:func] : nil;
+ NSString *msg = [self.formatter stringForFunc:fname
+ withFormat:fmt
+ valist:args
+ level:level];
+ if (msg && [self.filter filterAllowsMessage:msg level:level])
+ [self.writer logMessage:msg level:level];
+ }
+ @catch (id e) {
+ // Ignored
+ }
+}
+
+@end
+
+FIRMessagingLogger *FIRMessagingSharedLogger() {
+ static dispatch_once_t onceToken;
+ static FIRMessagingLogger *logger;
+ dispatch_once(&onceToken, ^{
+ logger = [FIRMessagingLogger standardLogger];
+ });
+
+ return logger;
+}
diff --git a/Firebase/Messaging/FIRMessagingPacketQueue.h b/Firebase/Messaging/FIRMessagingPacketQueue.h
new file mode 100644
index 0000000..1f528ab
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingPacketQueue.h
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+@interface FIRMessagingPacket : NSObject
+
++ (FIRMessagingPacket *)packetWithTag:(int8_t)tag rmqId:(NSString *)rmqId data:(NSData *)data;
+
+@property(nonatomic, readonly, strong) NSData *data;
+@property(nonatomic, readonly, assign) int8_t tag;
+// not sent over the wire required for bookkeeping
+@property(nonatomic, readonly, assign) NSString *rmqId;
+
+@end
+
+
+/**
+ * A queue of the packets(protos) that need to be send over the wire.
+ */
+@interface FIRMessagingPacketQueue : NSObject
+
+@property(nonatomic, readonly, assign) NSUInteger count;
+@property(nonatomic, readonly, assign) BOOL isEmpty;
+
+- (void)push:(FIRMessagingPacket *)packet;
+- (void)pushHead:(FIRMessagingPacket *)packet;
+- (FIRMessagingPacket *)pop;
+
+@end
diff --git a/Firebase/Messaging/FIRMessagingPacketQueue.m b/Firebase/Messaging/FIRMessagingPacketQueue.m
new file mode 100644
index 0000000..2b3410a
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingPacketQueue.m
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRMessagingPacketQueue.h"
+
+#import "FIRMessagingDefines.h"
+
+@interface FIRMessagingPacket ()
+
+@property(nonatomic, readwrite, strong) NSData *data;
+@property(nonatomic, readwrite, assign) int8_t tag;
+@property(nonatomic, readwrite, assign) NSString *rmqId;
+
+@end
+
+@implementation FIRMessagingPacket
+
++ (FIRMessagingPacket *)packetWithTag:(int8_t)tag rmqId:(NSString *)rmqId data:(NSData *)data {
+ return [[self alloc] initWithTag:tag rmqId:rmqId data:data];
+}
+
+- (instancetype)init {
+ FIRMessagingInvalidateInitializer();
+}
+
+- (instancetype)initWithTag:(int8_t)tag rmqId:(NSString *)rmqId data:(NSData *)data {
+ self = [super init];
+ if (self != nil) {
+ _data = data;
+ _tag = tag;
+ _rmqId = rmqId;
+ }
+ return self;
+}
+
+- (NSString *)description {
+ if ([self.rmqId length]) {
+ return [NSString stringWithFormat:@"<Packet: Tag - %d, Length - %lu>, RmqId - %@",
+ self.tag, _FIRMessaging_UL(self.data.length), self.rmqId];
+ } else {
+ return [NSString stringWithFormat:@"<Packet: Tag - %d, Length - %lu>",
+ self.tag, _FIRMessaging_UL(self.data.length)];
+ }
+}
+
+@end
+
+@interface FIRMessagingPacketQueue ()
+
+@property(nonatomic, readwrite, strong) NSMutableArray *packetsContainer;
+
+@end
+
+
+@implementation FIRMessagingPacketQueue;
+
+- (id)init {
+ self = [super init];
+ if (self) {
+ _packetsContainer = [[NSMutableArray alloc] init];
+ }
+ return self;
+}
+
+- (BOOL)isEmpty {
+ return self.packetsContainer.count == 0;
+}
+
+- (NSUInteger)count {
+ return self.packetsContainer.count;
+}
+
+- (void)push:(FIRMessagingPacket *)packet {
+ [self.packetsContainer addObject:packet];
+}
+
+- (void)pushHead:(FIRMessagingPacket *)packet {
+ [self.packetsContainer insertObject:packet atIndex:0];
+}
+
+- (FIRMessagingPacket *)pop {
+ if (!self.isEmpty) {
+ FIRMessagingPacket *packet = self.packetsContainer[0];
+ [self.packetsContainer removeObjectAtIndex:0];
+ return packet;
+ }
+ return nil;
+}
+
+@end
diff --git a/Firebase/Messaging/FIRMessagingPendingTopicsList.h b/Firebase/Messaging/FIRMessagingPendingTopicsList.h
new file mode 100644
index 0000000..c5a306a
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingPendingTopicsList.h
@@ -0,0 +1,118 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+#import "FIRMessagingTopicsCommon.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * Represents a single batch of topics, with the same action.
+ *
+ * Topic operations which have the same action (subscribe or unsubscribe) can be executed
+ * simultaneously, as the order of operations do not matter with the same action. The set of
+ * topics is unique, as it doesn't make sense to apply the same action to the same topic
+ * repeatedly; the result would be the same as the first time.
+ */
+@interface FIRMessagingTopicBatch : NSObject <NSCoding>
+
+@property(nonatomic, readonly, assign) FIRMessagingTopicAction action;
+@property(nonatomic, readonly, copy) NSMutableSet <NSString *> *topics;
+
+- (instancetype)init NS_UNAVAILABLE;
+- (instancetype)initWithAction:(FIRMessagingTopicAction)action NS_DESIGNATED_INITIALIZER;
+
+@end
+
+@class FIRMessagingPendingTopicsList;
+/**
+ * This delegate must be supplied to the instance of FIRMessagingPendingTopicsList, via the
+ * @cdelegate property. It lets the
+ * pending topics list know whether or not it can begin making requests via
+ * @c-pendingTopicsListCanRequestTopicUpdates:, and handles the request to actually
+ * perform the topic operation. The delegate also handles when the pending topics list is updated,
+ * so that it can be archived or persisted.
+ *
+ * @see FIRMessagingPendingTopicsList
+ */
+@protocol FIRMessagingPendingTopicsListDelegate <NSObject>
+
+- (void)pendingTopicsList:(FIRMessagingPendingTopicsList *)list
+ requestedUpdateForTopic:(NSString *)topic
+ action:(FIRMessagingTopicAction)action
+ completion:(FIRMessagingTopicOperationCompletion)completion;
+- (void)pendingTopicsListDidUpdate:(FIRMessagingPendingTopicsList *)list;
+- (BOOL)pendingTopicsListCanRequestTopicUpdates:(FIRMessagingPendingTopicsList *)list;
+
+@end
+
+/**
+ * FIRMessagingPendingTopicsList manages a list of topic subscription updates, batched by the same
+ * action (subscribe or unsubscribe). The list roughly maintains the order of the topic operations,
+ * batched together whenever the topic action (subscribe or unsubscribe) changes.
+ *
+ * Topics operations are batched by action because it is safe to perform the same topic action
+ * (subscribe or unsubscribe) on many topics simultaneously. After each batch is successfully
+ * completed, the next batch operations can begin.
+ *
+ * When asked to resume its operations, FIRMessagingPendingTopicsList will begin performing updates
+ * of its current batch of topics. For example, it may begin subscription operations for topics
+ * [A, B, C] simultaneously.
+ *
+ * When the current batch is completed, the next batch of operations will be started. For example
+ * the list may begin unsubscribe operations for [D, A, E]. Note that because A is in both batches,
+ * A will be correctly subscribed in the first batch, then unsubscribed as part of the second batch
+ * of operations. Without batching, it would be ambiguous whether A's subscription operation or the
+ * unsubscription operation would be completed first.
+ *
+ * An app can subscribe and unsubscribe from many topics, and this class helps persist the pending
+ * topics and perform the operation safely and correctly.
+ *
+ * When a topic fails to subscribe or unsubscribe due to a network error, it is considered a
+ * recoverable error, and so it remains in the current batch until it is succesfully completed.
+ * Topic updates are completed when they either (a) succeed, (b) are cancelled, or (c) result in an
+ * unrecoverable error. Any error outside of `NSURLErrorDomain` is considered an unrecoverable
+ * error.
+ *
+ * In addition to maintaining the list of pending topic updates, FIRMessagingPendingTopicsList also
+ * can track completion handlers for topic operations.
+ *
+ * @discussion Completion handlers for topic updates are not maintained if it was restored from a
+ * keyed archive. They are only called if the topic operation finished within the same app session.
+ *
+ * You must supply an object conforming to FIRMessagingPendingTopicsListDelegate in order for the
+ * topic operations to execute.
+ *
+ * @see FIRMessagingPendingTopicsListDelegate
+ */
+@interface FIRMessagingPendingTopicsList : NSObject <NSCoding>
+
+@property(nonatomic, weak) NSObject <FIRMessagingPendingTopicsListDelegate> *delegate;
+
+@property(nonatomic, readonly, strong, nullable) NSDate *archiveDate;
+@property(nonatomic, readonly) NSUInteger numberOfBatches;
+
+
+- (instancetype)init NS_DESIGNATED_INITIALIZER;
+- (void)addOperationForTopic:(NSString *)topic
+ withAction:(FIRMessagingTopicAction)action
+ completion:(nullable FIRMessagingTopicOperationCompletion)completion;
+- (void)resumeOperationsIfNeeded;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Messaging/FIRMessagingPendingTopicsList.m b/Firebase/Messaging/FIRMessagingPendingTopicsList.m
new file mode 100644
index 0000000..792090e
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingPendingTopicsList.m
@@ -0,0 +1,261 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRMessagingPendingTopicsList.h"
+
+#import "FIRMessaging_Private.h"
+#import "FIRMessagingLogger.h"
+#import "FIRMessagingPubSub.h"
+
+#import "FIRMessagingDefines.h"
+
+NSString *const kPendingTopicBatchActionKey = @"action";
+NSString *const kPendingTopicBatchTopicsKey = @"topics";
+
+NSString *const kPendingBatchesEncodingKey = @"batches";
+NSString *const kPendingTopicsTimestampEncodingKey = @"ts";
+
+#pragma mark - FIRMessagingTopicBatch
+
+@interface FIRMessagingTopicBatch ()
+
+@property(nonatomic, strong, nonnull) NSMutableDictionary
+ <NSString *, NSMutableArray <FIRMessagingTopicOperationCompletion> *> *topicHandlers;
+
+@end
+
+@implementation FIRMessagingTopicBatch
+
+- (instancetype)initWithAction:(FIRMessagingTopicAction)action {
+ if (self = [super init]) {
+ _action = action;
+ _topics = [NSMutableSet set];
+ _topicHandlers = [NSMutableDictionary dictionary];
+ }
+ return self;
+}
+
+#pragma mark NSCoding
+
+- (void)encodeWithCoder:(NSCoder *)aCoder {
+ [aCoder encodeInteger:self.action forKey:kPendingTopicBatchActionKey];
+ [aCoder encodeObject:self.topics forKey:kPendingTopicBatchTopicsKey];
+}
+
+- (instancetype)initWithCoder:(NSCoder *)aDecoder {
+
+ // Ensure that our integer -> enum casting is safe
+ NSInteger actionRawValue = [aDecoder decodeIntegerForKey:kPendingTopicBatchActionKey];
+ FIRMessagingTopicAction action = FIRMessagingTopicActionSubscribe;
+ if (actionRawValue == FIRMessagingTopicActionUnsubscribe) {
+ action = FIRMessagingTopicActionUnsubscribe;
+ }
+
+ if (self = [self initWithAction:action]) {
+ NSSet *topics = [aDecoder decodeObjectForKey:kPendingTopicBatchTopicsKey];
+ if ([topics isKindOfClass:[NSSet class]]) {
+ _topics = [topics mutableCopy];
+ }
+ _topicHandlers = [NSMutableDictionary dictionary];
+ }
+ return self;
+}
+
+@end
+
+#pragma mark - FIRMessagingPendingTopicsList
+
+@interface FIRMessagingPendingTopicsList ()
+
+@property(nonatomic, readwrite, strong) NSDate *archiveDate;
+@property(nonatomic, strong) NSMutableArray <FIRMessagingTopicBatch *> *topicBatches;
+
+@property(nonatomic, strong) FIRMessagingTopicBatch *currentBatch;
+@property(nonatomic, strong) NSMutableSet <NSString *> *topicsInFlight;
+
+@end
+
+@implementation FIRMessagingPendingTopicsList
+
+- (instancetype)init {
+ if (self = [super init]) {
+ _topicBatches = [NSMutableArray array];
+ _topicsInFlight = [NSMutableSet set];
+ }
+ return self;
+}
+
++ (void)pruneTopicBatches:(NSMutableArray <FIRMessagingTopicBatch *> *)topicBatches {
+ // For now, just remove empty batches. In the future we can use this to make the subscriptions
+ // more efficient, by actually pruning topic actions that cancel each other out, for example.
+ for (NSInteger i = topicBatches.count-1; i >= 0; i--) {
+ FIRMessagingTopicBatch *batch = topicBatches[i];
+ if (batch.topics.count == 0) {
+ [topicBatches removeObjectAtIndex:i];
+ }
+ }
+}
+
+#pragma mark NSCoding
+
+- (void)encodeWithCoder:(NSCoder *)aCoder {
+ [aCoder encodeObject:[NSDate date] forKey:kPendingTopicsTimestampEncodingKey];
+ [aCoder encodeObject:self.topicBatches forKey:kPendingBatchesEncodingKey];
+}
+
+- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder {
+
+ if (self = [self init]) {
+ _archiveDate = [aDecoder decodeObjectForKey:kPendingTopicsTimestampEncodingKey];
+ NSArray *archivedBatches = [aDecoder decodeObjectForKey:kPendingBatchesEncodingKey];
+ if (archivedBatches) {
+ _topicBatches = [archivedBatches mutableCopy];
+ [FIRMessagingPendingTopicsList pruneTopicBatches:_topicBatches];
+ }
+ _topicsInFlight = [NSMutableSet set];
+ }
+ return self;
+}
+
+#pragma mark Getters
+
+- (NSUInteger)numberOfBatches {
+ return self.topicBatches.count;
+}
+
+#pragma mark Adding/Removing topics
+
+- (void)addOperationForTopic:(NSString *)topic
+ withAction:(FIRMessagingTopicAction)action
+ completion:(nullable FIRMessagingTopicOperationCompletion)completion {
+
+ FIRMessagingTopicBatch *lastBatch = nil;
+ @synchronized (self) {
+ lastBatch = self.topicBatches.lastObject;
+ if (!lastBatch || lastBatch.action != action) {
+ // There either was no last batch, or our last batch's action was not the same, so we have to
+ // create a new batch
+ lastBatch = [[FIRMessagingTopicBatch alloc] initWithAction:action];
+ [self.topicBatches addObject:lastBatch];
+ }
+ BOOL topicExistedBefore = ([lastBatch.topics member:topic] != nil);
+ if (!topicExistedBefore) {
+ [lastBatch.topics addObject:topic];
+ [self.delegate pendingTopicsListDidUpdate:self];
+ }
+ // Add the completion handler to the batch
+ if (completion) {
+ NSMutableArray *handlers = lastBatch.topicHandlers[topic];
+ if (!handlers) {
+ handlers = [NSMutableArray arrayWithCapacity:1];
+ }
+ [handlers addObject:completion];
+ }
+ if (!self.currentBatch) {
+ self.currentBatch = lastBatch;
+ }
+ // This may have been the first topic added, or was added to an ongoing batch
+ if (self.currentBatch == lastBatch && !topicExistedBefore) {
+ // Add this topic to our ongoing operations
+ FIRMessaging_WEAKIFY(self);
+ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
+ FIRMessaging_STRONGIFY(self);
+ [self resumeOperationsIfNeeded];
+ });
+ }
+ }
+}
+
+- (void)resumeOperationsIfNeeded {
+ @synchronized (self) {
+ // If current batch is not set, set it now
+ if (!self.currentBatch) {
+ self.currentBatch = self.topicBatches.firstObject;
+ }
+ if (self.currentBatch.topics.count == 0) {
+ return;
+ }
+ if (!self.delegate) {
+ FIRMessagingLoggerError(kFIRMessagingMessageCodePendingTopicsList000,
+ @"Attempted to update pending topics without a delegate");
+ return;
+ }
+ if (![self.delegate pendingTopicsListCanRequestTopicUpdates:self]) {
+ return;
+ }
+ for (NSString *topic in self.currentBatch.topics) {
+ if ([self.topicsInFlight member:topic]) {
+ // This topic is already active, so skip
+ continue;
+ }
+ [self beginUpdateForCurrentBatchTopic:topic];
+ }
+ }
+}
+
+- (BOOL)subscriptionErrorIsRecoverable:(NSError *)error {
+ return [error.domain isEqualToString:NSURLErrorDomain];
+}
+
+- (void)beginUpdateForCurrentBatchTopic:(NSString *)topic {
+
+ @synchronized (self) {
+ [self.topicsInFlight addObject:topic];
+ }
+ FIRMessaging_WEAKIFY(self);
+ [self.delegate pendingTopicsList:self
+ requestedUpdateForTopic:topic
+ action:self.currentBatch.action
+ completion:^(FIRMessagingTopicOperationResult result, NSError * error) {
+ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
+ FIRMessaging_STRONGIFY(self);
+ @synchronized (self) {
+ [self.topicsInFlight removeObject:topic];
+
+ BOOL recoverableError = [self subscriptionErrorIsRecoverable:error];
+ if (result == FIRMessagingTopicOperationResultSucceeded ||
+ result == FIRMessagingTopicOperationResultCancelled ||
+ !recoverableError) {
+ // Notify our handlers and remove the topic from our batch
+ NSMutableArray *handlers = self.currentBatch.topicHandlers[topic];
+ if (handlers.count) {
+ dispatch_async(dispatch_get_main_queue(), ^{
+ for (FIRMessagingTopicOperationCompletion handler in handlers) {
+ handler(result, error);
+ }
+ [handlers removeAllObjects];
+ });
+ }
+ [self.currentBatch.topics removeObject:topic];
+ [self.currentBatch.topicHandlers removeObjectForKey:topic];
+ if (self.currentBatch.topics.count == 0) {
+ // All topic updates successfully finished in this batch, move on to the next batch
+ [self.topicBatches removeObject:self.currentBatch];
+ self.currentBatch = nil;
+ }
+ [self.delegate pendingTopicsListDidUpdate:self];
+ FIRMessaging_WEAKIFY(self)
+ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
+ FIRMessaging_STRONGIFY(self)
+ [self resumeOperationsIfNeeded];
+ });
+ }
+ }
+ });
+ }];
+}
+
+@end
diff --git a/Firebase/Messaging/FIRMessagingPersistentSyncMessage.h b/Firebase/Messaging/FIRMessagingPersistentSyncMessage.h
new file mode 100644
index 0000000..5a48e99
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingPersistentSyncMessage.h
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+@interface FIRMessagingPersistentSyncMessage : NSObject
+
+@property(nonatomic, readonly, strong) NSString *rmqID;
+@property(nonatomic, readwrite, assign) BOOL apnsReceived;
+@property(nonatomic, readwrite, assign) BOOL mcsReceived;
+@property(nonatomic, readonly, assign) int64_t expirationTime;
+
+- (instancetype)initWithRMQID:(NSString *)rmqID expirationTime:(int64_t)expirationTime;
+
+@end
diff --git a/Firebase/Messaging/FIRMessagingPersistentSyncMessage.m b/Firebase/Messaging/FIRMessagingPersistentSyncMessage.m
new file mode 100644
index 0000000..bf7d05b
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingPersistentSyncMessage.m
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRMessagingPersistentSyncMessage.h"
+
+#import "FIRMessagingDefines.h"
+
+@interface FIRMessagingPersistentSyncMessage ()
+
+@property(nonatomic, readwrite, strong) NSString *rmqID;
+@property(nonatomic, readwrite, assign) int64_t expirationTime;
+
+@end
+
+@implementation FIRMessagingPersistentSyncMessage
+
+- (instancetype)init {
+ FIRMessagingInvalidateInitializer();
+}
+
+- (instancetype)initWithRMQID:(NSString *)rmqID expirationTime:(int64_t)expirationTime {
+ self = [super init];
+ if (self) {
+ _rmqID = [rmqID copy];
+ _expirationTime = expirationTime;
+ }
+ return self;
+}
+
+- (NSString *)description {
+ NSString *classDescription = NSStringFromClass([self class]);
+ NSDate *date = [NSDate dateWithTimeIntervalSince1970:self.expirationTime];
+ return [NSString stringWithFormat:@"%@: (rmqID: %@, apns: %d, mcs: %d, expiry: %@",
+ classDescription, self.rmqID, self.mcsReceived, self.apnsReceived, date];
+}
+
+- (NSString *)debugDescription {
+ return [self description];
+}
+
+@end
diff --git a/Firebase/Messaging/FIRMessagingPubSub.h b/Firebase/Messaging/FIRMessagingPubSub.h
new file mode 100644
index 0000000..3a03494
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingPubSub.h
@@ -0,0 +1,148 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRMessagingTopicsCommon.h"
+
+@class FIRMessagingClient;
+@class FIRMessagingPubSubCache;
+
+/**
+ * FIRMessagingPubSub provides a publish-subscribe model for sending FIRMessaging topic messages.
+ *
+ * An app can subscribe to different topics defined by the
+ * developer. The app server can then send messages to the subscribed devices
+ * without having to maintain topic-subscribers mapping. Topics do not
+ * need to be explicitly created before subscribing or publishing&mdash;they
+ * are automatically created when publishing or subscribing.
+ *
+ * Messages published to the topic will be received as regular FIRMessaging messages
+ * with `"from"` set to `"/topics/myTopic"`.
+ *
+ * Only topic names that match the pattern `"/topics/[a-zA-Z0-9-_.~%]{1,900}"`
+ * are allowed for subscribing and publishing.
+ */
+@interface FIRMessagingPubSub : NSObject
+
+@property(nonatomic, readonly, strong) FIRMessagingPubSubCache *cache;
+@property(nonatomic, readonly, strong) FIRMessagingClient *client;
+
+/**
+ * Initializes an instance of FIRMessagingPubSub.
+ *
+ * @return An instance of FIRMessagingPubSub.
+ */
+- (instancetype)initWithClient:(FIRMessagingClient *)client NS_DESIGNATED_INITIALIZER;
+
+/**
+ * Subscribes an app instance to a topic, enabling it to receive messages
+ * sent to that topic.
+ *
+ * This is an asynchronous call. If subscription fails, FIRMessaging
+ * invokes the completion callback with the appropriate error.
+ *
+ * @see FIRMessagingPubSub unsubscribeWithToken:topic:handler:
+ *
+ * @param token The registration token as received from the InstanceID
+ * library for a given `authorizedEntity` and "gcm" scope.
+ * @param topic The topic to subscribe to. Should be of the form
+ * `"/topics/<topic-name>"`.
+ * @param handler The callback handler invoked when the subscribe call
+ * ends. In case of success, a nil error is returned. Otherwise,
+ * an appropriate error object is returned.
+ * @discussion This method is thread-safe. However, it is not guaranteed to
+ * return on the main thread.
+ */
+- (void)subscribeWithToken:(NSString *)token
+ topic:(NSString *)topic
+ options:(NSDictionary *)options
+ handler:(FIRMessagingTopicOperationCompletion)handler;
+
+
+/**
+ * Unsubscribes an app instance from a topic, stopping it from receiving
+ * any further messages sent to that topic.
+ *
+ * This is an asynchronous call. If the attempt to unsubscribe fails,
+ * we invoke the `completion` callback passed in with an appropriate error.
+ *
+ * @param token The token used to subscribe to this topic.
+ * @param topic The topic to unsubscribe from. Should be of the form
+ * `"/topics/<topic-name>"`.
+ * @param handler The handler that is invoked once the unsubscribe call ends.
+ * In case of success, nil error is returned. Otherwise, an
+ * appropriate error object is returned.
+ * @discussion This method is thread-safe. However, it is not guaranteed to
+ * return on the main thread.
+ */
+- (void)unsubscribeWithToken:(NSString *)token
+ topic:(NSString *)topic
+ options:(NSDictionary *)options
+ handler:(FIRMessagingTopicOperationCompletion)handler;
+
+/**
+ * Asynchronously subscribe to the topic. Adds to the pending list of topic operations.
+ * Retry in case of failures. This makes a repeated attempt to subscribe to the topic
+ * as compared to the `subscribe` method above which tries once.
+ *
+ * @param topic The topic name to subscribe to. Should be of the form `"/topics/<topic-name>"`.
+ */
+- (void)subscribeToTopic:(NSString *)topic;
+
+/**
+ * Asynchronously unsubscribe from the topic. Adds to the pending list of topic operations.
+ * Retry in case of failures. This makes a repeated attempt to unsubscribe from the topic
+ * as compared to the `unsubscribe` method above which tries once.
+ *
+ * @param topic The topic name to unsubscribe from. Should be of the form `"/topics/<topic-name>"`.
+ */
+- (void)unsubscribeFromTopic:(NSString *)topic;
+
+/**
+ * Schedule subscriptions sync.
+ *
+ * @param immediately YES if the sync should be scheduled immediately else NO if we can delay
+ * the sync.
+ */
+- (void)scheduleSync:(BOOL)immediately;
+
+/**
+ * Adds the "/topics/" prefix to the topic.
+ *
+ * @param topic The topic to add the prefix to.
+ *
+ * @return The new topic name with the "/topics/" prefix added.
+ */
++ (NSString *)addPrefixToTopic:(NSString *)topic;
+
+/**
+ * Check if the topic name has "/topics/" prefix.
+ *
+ * @param topic The topic name to verify.
+ *
+ * @return YES if the topic name has "/topics/" prefix else NO.
+ */
++ (BOOL)hasTopicsPrefix:(NSString *)topic;
+
+/**
+ * Check if it's a valid topic name. This includes "/topics/" prefix in the topic name.
+ *
+ * @param topic The topic name to verify.
+ *
+ * @return YES if the topic name satisfies the regex "/topics/[a-zA-Z0-9-_.~%]{1,900}".
+ */
++ (BOOL)isValidTopicWithPrefix:(NSString *)topic;
+
+@end
diff --git a/Firebase/Messaging/FIRMessagingPubSub.m b/Firebase/Messaging/FIRMessagingPubSub.m
new file mode 100644
index 0000000..c8293e0
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingPubSub.m
@@ -0,0 +1,278 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRMessagingPubSub.h"
+
+#import "FIRMessaging.h"
+#import "FIRMessagingClient.h"
+#import "FIRMessagingDefines.h"
+#import "FIRMessagingLogger.h"
+#import "FIRMessagingPendingTopicsList.h"
+#import "FIRMessagingUtilities.h"
+#import "FIRMessaging_Private.h"
+#import "NSDictionary+FIRMessaging.h"
+#import "NSError+FIRMessaging.h"
+
+static NSString *const kPendingSubscriptionsListKey =
+ @"com.firebase.messaging.pending-subscriptions";
+
+@interface FIRMessagingPubSub () <FIRMessagingPendingTopicsListDelegate>
+
+@property(nonatomic, readwrite, strong) FIRMessagingPendingTopicsList *pendingTopicUpdates;
+@property(nonatomic, readwrite, strong) FIRMessagingClient *client;
+
+@end
+
+@implementation FIRMessagingPubSub
+
+- (instancetype)init {
+ FIRMessagingInvalidateInitializer();
+ // Need this to disable an Xcode warning.
+ return [self initWithClient:nil];
+}
+
+- (instancetype)initWithClient:(FIRMessagingClient *)client {
+ self = [super init];
+ if (self) {
+ _client = client;
+ [self restorePendingTopicsList];
+ }
+ return self;
+}
+
+- (void)subscribeWithToken:(NSString *)token
+ topic:(NSString *)topic
+ options:(NSDictionary *)options
+ handler:(FIRMessagingTopicOperationCompletion)handler {
+ _FIRMessagingDevAssert([token length], @"FIRMessaging error no token specified");
+ _FIRMessagingDevAssert([topic length], @"FIRMessaging error Invalid empty topic specified");
+ if (!self.client) {
+ handler(FIRMessagingTopicOperationResultError,
+ [NSError errorWithFCMErrorCode:kFIRMessagingErrorCodePubSubFIRMessagingNotSetup]);
+ return;
+ }
+
+ token = [token copy];
+ topic = [topic copy];
+
+ if (![options count]) {
+ options = @{};
+ }
+
+ if (![[self class] isValidTopicWithPrefix:topic]) {
+ FIRMessagingLoggerError(kFIRMessagingMessageCodePubSub000,
+ @"Invalid FIRMessaging Pubsub topic %@", topic);
+ handler(FIRMessagingTopicOperationResultError,
+ [NSError errorWithFCMErrorCode:kFIRMessagingErrorCodePubSubInvalidTopic]);
+ return;
+ }
+
+ if (![self verifyPubSubOptions:options]) {
+ // we do not want to quit even if options have some invalid values.
+ FIRMessagingLoggerError(kFIRMessagingMessageCodePubSub001,
+ @"Invalid options passed to FIRMessagingPubSub with non-string keys or "
+ "values.");
+ }
+ // copy the dictionary would trim non-string keys or values if any.
+ options = [options fcm_trimNonStringValues];
+
+ [self.client updateSubscriptionWithToken:token
+ topic:topic
+ options:options
+ shouldDelete:NO
+ handler:
+ ^void(FIRMessagingTopicOperationResult result, NSError * error) {
+
+ handler(result, error);
+ }];
+}
+
+- (void)unsubscribeWithToken:(NSString *)token
+ topic:(NSString *)topic
+ options:(NSDictionary *)options
+ handler:(FIRMessagingTopicOperationCompletion)handler {
+ _FIRMessagingDevAssert([token length], @"FIRMessaging error no token specified");
+ _FIRMessagingDevAssert([topic length], @"FIRMessaging error Invalid empty topic specified");
+
+ if (!self.client) {
+ handler(FIRMessagingTopicOperationResultError,
+ [NSError errorWithFCMErrorCode:kFIRMessagingErrorCodePubSubFIRMessagingNotSetup]);
+ return;
+ }
+
+ token = [token copy];
+ topic = [topic copy];
+ if (![options count]) {
+ options = @{};
+ }
+
+ if (![[self class] isValidTopicWithPrefix:topic]) {
+ FIRMessagingLoggerError(kFIRMessagingMessageCodePubSub002,
+ @"Invalid FIRMessaging Pubsub topic %@", topic);
+ handler(FIRMessagingTopicOperationResultError,
+ [NSError errorWithFCMErrorCode:kFIRMessagingErrorCodePubSubInvalidTopic]);
+ return;
+ }
+ if (![self verifyPubSubOptions:options]) {
+ // we do not want to quit even if options have some invalid values.
+ FIRMessagingLoggerError(
+ kFIRMessagingMessageCodePubSub003,
+ @"Invalid options passed to FIRMessagingPubSub with non-string keys or values.");
+ }
+ // copy the dictionary would trim non-string keys or values if any.
+ options = [options fcm_trimNonStringValues];
+
+ [self.client updateSubscriptionWithToken:token
+ topic:topic
+ options:options
+ shouldDelete:YES
+ handler:
+ ^void(FIRMessagingTopicOperationResult result, NSError * error) {
+
+ handler(result, error);
+ }];
+}
+
+- (void)subscribeToTopic:(NSString *)topic {
+ [self.pendingTopicUpdates addOperationForTopic:topic
+ withAction:FIRMessagingTopicActionSubscribe
+ completion:nil];
+}
+
+- (void)unsubscribeFromTopic:(NSString *)topic {
+ [self.pendingTopicUpdates addOperationForTopic:topic
+ withAction:FIRMessagingTopicActionUnsubscribe
+ completion:nil];
+}
+
+- (void)scheduleSync:(BOOL)immediately {
+ NSString *fcmToken = [[FIRMessaging messaging] defaultFcmToken];
+ if (fcmToken.length) {
+ [self.pendingTopicUpdates resumeOperationsIfNeeded];
+ }
+}
+
+#pragma mark - FIRMessagingPendingTopicsListDelegate
+
+- (void)pendingTopicsList:(FIRMessagingPendingTopicsList *)list
+ requestedUpdateForTopic:(NSString *)topic
+ action:(FIRMessagingTopicAction)action
+ completion:(FIRMessagingTopicOperationCompletion)completion {
+
+ NSString *fcmToken = [[FIRMessaging messaging] defaultFcmToken];
+ if (action == FIRMessagingTopicActionSubscribe) {
+ [self subscribeWithToken:fcmToken topic:topic options:nil handler:completion];
+ } else {
+ [self unsubscribeWithToken:fcmToken topic:topic options:nil handler:completion];
+ }
+}
+
+- (void)pendingTopicsListDidUpdate:(FIRMessagingPendingTopicsList *)list {
+ [self archivePendingTopicsList:list];
+}
+
+- (BOOL)pendingTopicsListCanRequestTopicUpdates:(FIRMessagingPendingTopicsList *)list {
+ NSString *fcmToken = [[FIRMessaging messaging] defaultFcmToken];
+ return (fcmToken.length > 0);
+}
+
+#pragma mark - Storing Pending Topics
+
+- (void)archivePendingTopicsList:(FIRMessagingPendingTopicsList *)topicsList {
+ NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
+ NSData *pendingData = [NSKeyedArchiver archivedDataWithRootObject:topicsList];
+ [defaults setObject:pendingData forKey:kPendingSubscriptionsListKey];
+ [defaults synchronize];
+}
+
+- (void)restorePendingTopicsList {
+ NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
+ NSData *pendingData = [defaults objectForKey:kPendingSubscriptionsListKey];
+ FIRMessagingPendingTopicsList *subscriptions;
+ @try {
+ if (pendingData) {
+ subscriptions = [NSKeyedUnarchiver unarchiveObjectWithData:pendingData];
+ }
+ } @catch (NSException *exception) {
+ // Nothing we can do, just continue as if we don't have pending subscriptions
+ } @finally {
+ if (subscriptions) {
+ self.pendingTopicUpdates = subscriptions;
+ } else {
+ self.pendingTopicUpdates = [[FIRMessagingPendingTopicsList alloc] init];
+ }
+ self.pendingTopicUpdates.delegate = self;
+ }
+}
+
+#pragma mark - Private Helpers
+
+- (BOOL)verifyPubSubOptions:(NSDictionary *)options {
+ return ![options fcm_hasNonStringKeysOrValues];
+}
+
+#pragma mark - Topic Name Helpers
+
+static NSString *const kTopicsPrefix = @"/topics/";
+static NSString *const kTopicRegexPattern = @"/topics/([a-zA-Z0-9-_.~%]+)";
+
++ (NSString *)addPrefixToTopic:(NSString *)topic {
+ if (![self hasTopicsPrefix:topic]) {
+ return [NSString stringWithFormat:@"%@%@", kTopicsPrefix, topic];
+ } else {
+ return [topic copy];
+ }
+}
+
++ (BOOL)hasTopicsPrefix:(NSString *)topic {
+ return [topic hasPrefix:kTopicsPrefix];
+}
+
+/**
+ * Returns a regular expression for matching a topic sender.
+ *
+ * @return The topic matching regular expression
+ */
++ (NSRegularExpression *)topicRegex {
+ // Since this is a static regex pattern, we only only need to declare it once.
+ static NSRegularExpression *topicRegex;
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ NSError *error;
+ topicRegex =
+ [NSRegularExpression regularExpressionWithPattern:kTopicRegexPattern
+ options:NSRegularExpressionAnchorsMatchLines
+ error:&error];
+ });
+ return topicRegex;
+}
+
+/**
+ * Gets the class describing occurences of topic names and sender IDs in the sender.
+ *
+ * @param expression The topic expression used to generate a pubsub topic
+ *
+ * @return Representation of captured subexpressions in topic regular expression
+ */
++ (BOOL)isValidTopicWithPrefix:(NSString *)topic {
+ NSRange topicRange = NSMakeRange(0, topic.length);
+ NSRange regexMatchRange = [[self topicRegex] rangeOfFirstMatchInString:topic
+ options:NSMatchingAnchored
+ range:topicRange];
+ return NSEqualRanges(topicRange, regexMatchRange);
+}
+
+@end
diff --git a/Firebase/Messaging/FIRMessagingPubSubRegistrar.h b/Firebase/Messaging/FIRMessagingPubSubRegistrar.h
new file mode 100644
index 0000000..b51813f
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingPubSubRegistrar.h
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRMessagingTopicOperation.h"
+
+@class FIRMessagingCheckinService;
+
+@interface FIRMessagingPubSubRegistrar : NSObject
+
+/**
+ * Designated Initializer.
+ *
+ * @param checkinService The checkin service used to register with Checkin
+ * server.
+ *
+ * @return A new FIRMessagingPubSubRegistrar instance used to subscribe/unsubscribe.
+ */
+- (instancetype)initWithCheckinService:(FIRMessagingCheckinService *)checkinService;
+
+/**
+ * Stops all the subscription requests going on in parallel. This would
+ * invalidate all the handlers associated with the subscription requests.
+ */
+- (void)stopAllSubscriptionRequests;
+
+/**
+ * Update subscription status for a given topic with FIRMessaging's backend.
+ *
+ * @param topic The topic to subscribe to.
+ * @param token The registration token to be used.
+ * @param options The options to be passed in during subscription request.
+ * @param shouldDelete NO if the subscription is being added else YES if being
+ * removed.
+ * @param handler The handler invoked once the update subscription request
+ * finishes.
+ */
+- (void)updateSubscriptionToTopic:(NSString *)topic
+ withToken:(NSString *)token
+ options:(NSDictionary *)options
+ shouldDelete:(BOOL)shouldDelete
+ handler:(FIRMessagingTopicOperationCompletion)handler;
+
+@end
diff --git a/Firebase/Messaging/FIRMessagingPubSubRegistrar.m b/Firebase/Messaging/FIRMessagingPubSubRegistrar.m
new file mode 100644
index 0000000..6268302
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingPubSubRegistrar.m
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRMessagingPubSubRegistrar.h"
+
+#import "FIRMessagingCheckinService.h"
+#import "FIRMessagingDefines.h"
+#import "FIRMessagingPubSubRegistrar.h"
+#import "FIRMessagingTopicsCommon.h"
+#import "NSError+FIRMessaging.h"
+
+@interface FIRMessagingPubSubRegistrar ()
+
+@property(nonatomic, readwrite, strong) FIRMessagingCheckinService *checkinService;
+
+@property(nonatomic, readonly, strong) NSOperationQueue *topicOperations;
+// Common errors, instantiated, to avoid generating multiple copies
+@property(nonatomic, readwrite, strong) NSError *operationInProgressError;
+
+@end
+
+@implementation FIRMessagingPubSubRegistrar
+
+- (instancetype)init {
+ FIRMessagingInvalidateInitializer();
+}
+
+- (instancetype)initWithCheckinService:(FIRMessagingCheckinService *)checkinService {
+ self = [super init];
+ if (self) {
+ _checkinService = checkinService;
+ _topicOperations = [[NSOperationQueue alloc] init];
+ // Do 10 topic operations at a time; it's enough to keep the TCP connection to the host alive,
+ // saving hundreds of milliseconds on each request (compared to a serial queue).
+ _topicOperations.maxConcurrentOperationCount = 10;
+ }
+ return self;
+}
+
+- (void)stopAllSubscriptionRequests {
+ [self.topicOperations cancelAllOperations];
+}
+
+- (void)updateSubscriptionToTopic:(NSString *)topic
+ withToken:(NSString *)token
+ options:(NSDictionary *)options
+ shouldDelete:(BOOL)shouldDelete
+ handler:(FIRMessagingTopicOperationCompletion)handler {
+
+ FIRMessagingTopicAction action = FIRMessagingTopicActionSubscribe;
+ if (shouldDelete) {
+ action = FIRMessagingTopicActionUnsubscribe;
+ }
+ FIRMessagingTopicOperation *operation =
+ [[FIRMessagingTopicOperation alloc] initWithTopic:topic
+ action:action
+ token:token
+ options:options
+ checkinService:self.checkinService
+ completion:handler];
+ [self.topicOperations addOperation:operation];
+
+}
+
+@end
diff --git a/Firebase/Messaging/FIRMessagingReceiver.h b/Firebase/Messaging/FIRMessagingReceiver.h
new file mode 100644
index 0000000..6e4a693
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingReceiver.h
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRMessagingDataMessageManager.h"
+#import "FIRMessaging.h"
+
+@class FIRMessagingReceiver;
+@protocol FIRMessagingReceiverDelegate <NSObject>
+
+- (void)receiver:(nonnull FIRMessagingReceiver *)receiver
+ receivedRemoteMessage:(nonnull FIRMessagingRemoteMessage *)remoteMessage;
+
+@end
+
+
+@interface FIRMessagingReceiver : NSObject <FIRMessagingDataMessageManagerDelegate>
+@property(nonatomic, weak, nullable) id<FIRMessagingReceiverDelegate> delegate;
+@end
diff --git a/Firebase/Messaging/FIRMessagingReceiver.m b/Firebase/Messaging/FIRMessagingReceiver.m
new file mode 100644
index 0000000..7a99c92
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingReceiver.m
@@ -0,0 +1,141 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRMessagingReceiver.h"
+
+#import <UIKit/UIKit.h>
+
+#import "FIRMessaging.h"
+#import "FIRMessaging_Private.h"
+#import "FIRMessagingLogger.h"
+
+static NSString *const kUpstreamMessageIDUserInfoKey = @"messageID";
+static NSString *const kUpstreamErrorUserInfoKey = @"error";
+
+// Copied from Apple's header in case it is missing in some cases.
+#ifndef NSFoundationVersionNumber_iOS_9_x_Max
+#define NSFoundationVersionNumber_iOS_9_x_Max 1299
+#endif
+
+static int downstreamMessageID = 0;
+
+@implementation FIRMessagingReceiver
+
+#pragma mark - FIRMessagingDataMessageManager protocol
+
+- (void)didReceiveMessage:(NSDictionary *)message withIdentifier:(nullable NSString *)messageID {
+ if (![messageID length]) {
+ messageID = [[self class] nextMessageID];
+ }
+
+ if (floor(NSFoundationVersionNumber) > NSFoundationVersionNumber_iOS_9_x_Max) {
+ // Use delegate method for iOS 10
+ [self scheduleIos10NotificationForMessage:message withIdentifier:messageID];
+ } else {
+ // Post notification directly to AppDelegate handlers. This is valid pre-iOS 10.
+ [self scheduleNotificationForMessage:message];
+ }
+}
+
+- (void)willSendDataMessageWithID:(NSString *)messageID error:(NSError *)error {
+ NSNotification *notification;
+ if (error) {
+ NSDictionary *userInfo = @{
+ kUpstreamMessageIDUserInfoKey : [messageID copy],
+ kUpstreamErrorUserInfoKey : error
+ };
+ notification = [NSNotification notificationWithName:FIRMessagingSendErrorNotification
+ object:nil
+ userInfo:userInfo];
+ [[NSNotificationQueue defaultQueue] enqueueNotification:notification postingStyle:NSPostASAP];
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeReceiver000,
+ @"Fail to send upstream message: %@ error: %@", messageID, error);
+ } else {
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeReceiver001, @"Will send upstream message: %@",
+ messageID);
+ }
+}
+
+- (void)didSendDataMessageWithID:(NSString *)messageID {
+ // invoke the callbacks asynchronously
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeReceiver002, @"Did send upstream message: %@",
+ messageID);
+ NSNotification * notification =
+ [NSNotification notificationWithName:FIRMessagingSendSuccessNotification
+ object:nil
+ userInfo:@{ kUpstreamMessageIDUserInfoKey : [messageID copy] }];
+
+ [[NSNotificationQueue defaultQueue] enqueueNotification:notification postingStyle:NSPostASAP];
+}
+
+- (void)didDeleteMessagesOnServer {
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeReceiver003,
+ @"Will send deleted messages notification");
+ NSNotification * notification =
+ [NSNotification notificationWithName:FIRMessagingMessagesDeletedNotification
+ object:nil];
+
+ [[NSNotificationQueue defaultQueue] enqueueNotification:notification postingStyle:NSPostASAP];
+}
+
+#pragma mark - Private Helpers
+// As the new UserNotifications framework in iOS 10 doesn't support constructor/mutation for
+// UNNotification object, FCM can't inject the message to the app with UserNotifications framework.
+// Define our own protocol, which means app developers need to implement two interfaces to receive
+// display notifications and data messages respectively for devices running iOS 10 or above. Devices
+// running iOS 9 or below are not affected.
+- (void)scheduleIos10NotificationForMessage:(NSDictionary *)message
+ withIdentifier:(NSString *)messageID {
+ FIRMessagingRemoteMessage *wrappedMessage = [[FIRMessagingRemoteMessage alloc] init];
+ // TODO: wrap title, body, badge and other fields
+ wrappedMessage.appData = [message copy];
+ [self.delegate receiver:self receivedRemoteMessage:wrappedMessage];
+}
+
+- (void)scheduleNotificationForMessage:(NSDictionary *)message {
+ SEL newNotificationSelector =
+ @selector(application:didReceiveRemoteNotification:fetchCompletionHandler:);
+ SEL oldNotificationSelector = @selector(application:didReceiveRemoteNotification:);
+
+ dispatch_async(dispatch_get_main_queue(), ^{
+ id<UIApplicationDelegate> appDelegate = [[UIApplication sharedApplication] delegate];
+ if ([appDelegate respondsToSelector:newNotificationSelector]) {
+ // Try the new remote notification callback
+ [appDelegate application:[UIApplication sharedApplication]
+ didReceiveRemoteNotification:message
+ fetchCompletionHandler:^(UIBackgroundFetchResult result) {}];
+
+ } else if ([appDelegate respondsToSelector:oldNotificationSelector]) {
+ // Try the old remote notification callback
+ [appDelegate application:
+ [UIApplication sharedApplication] didReceiveRemoteNotification:message];
+
+ } else {
+ FIRMessagingLoggerError(kFIRMessagingMessageCodeReceiver005,
+ @"None of the remote notification callbacks implemented by "
+ @"UIApplicationDelegate");
+ }
+ });
+}
+
++ (NSString *)nextMessageID {
+ @synchronized (self) {
+ ++downstreamMessageID;
+ return [NSString stringWithFormat:@"gcm-%d", downstreamMessageID];
+ }
+}
+
+@end
diff --git a/Firebase/Messaging/FIRMessagingRegistrar.h b/Firebase/Messaging/FIRMessagingRegistrar.h
new file mode 100644
index 0000000..5b60437
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingRegistrar.h
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRMessagingCheckinService.h"
+#import "FIRMessagingTopicsCommon.h"
+
+@class FIRMessagingCheckinStore;
+@class FIRMessagingPubSubRegistrar;
+
+/**
+ * Handle the registration process for the client. Fetch checkin information from the Checkin
+ * service if not cached on the device and then try to register the client with FIRMessaging backend.
+ */
+@interface FIRMessagingRegistrar : NSObject
+
+@property(nonatomic, readonly, strong) FIRMessagingPubSubRegistrar *pubsubRegistrar;
+@property(nonatomic, readonly, strong) NSString *deviceAuthID;
+@property(nonatomic, readonly, strong) NSString *secretToken;
+
+/**
+ * Initialize a FIRMessaging Registrar.
+ *
+ * @return A FIRMessaging Registrar object.
+ */
+- (instancetype)init NS_DESIGNATED_INITIALIZER;
+
+#pragma mark - Checkin
+
+/**
+ * Try to load checkin info from the disk if not currently loaded into memory.
+ *
+ * @return YES if successfully loaded valid checkin info to memory else NO.
+ */
+- (BOOL)tryToLoadValidCheckinInfo;
+
+/**
+ * Check if we have a valid checkin info in memory.
+ *
+ * @return YES if we have valid checkin info in memory else NO.
+ */
+- (BOOL)hasValidCheckinInfo;
+
+#pragma mark - Subscribe/Unsubscribe
+
+/**
+ * Update the subscription for a given topic for the client.
+ *
+ * @param topic The topic for which the subscription should be updated.
+ * @param token The registration token to be used by the client.
+ * @param options The extra options if any being passed as part of
+ * subscription request.
+ * @param shouldDelete YES if we want to delete an existing subscription else NO
+ * if we want to create a new subscription.
+ * @param handler The handler to invoke once the subscription request is
+ * complete.
+ */
+- (void)updateSubscriptionToTopic:(NSString *)topic
+ withToken:(NSString *)token
+ options:(NSDictionary *)options
+ shouldDelete:(BOOL)shouldDelete
+ handler:(FIRMessagingTopicOperationCompletion)handler;
+
+/**
+ * Cancel all subscription requests as well as any requests to checkin. Note if
+ * there are subscription requests waiting on checkin to complete those requests
+ * would be marked as stale and be NO-OP's if they happen in the future.
+ *
+ * Also note this is a one time operation, you should only call this if you want
+ * to immediately stop all requests and deallocate the registrar. After calling
+ * this once you would no longer be able to use this registrar object.
+ */
+- (void)cancelAllRequests;
+
+@end
diff --git a/Firebase/Messaging/FIRMessagingRegistrar.m b/Firebase/Messaging/FIRMessagingRegistrar.m
new file mode 100644
index 0000000..ab57b9e
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingRegistrar.m
@@ -0,0 +1,112 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRMessagingRegistrar.h"
+
+#import "FIRMessagingDefines.h"
+#import "FIRMessagingLogger.h"
+#import "FIRMessagingPubSubRegistrar.h"
+#import "FIRMessagingUtilities.h"
+#import "NSError+FIRMessaging.h"
+
+@interface FIRMessagingRegistrar ()
+
+@property(nonatomic, readwrite, assign) BOOL stopAllSubscriptions;
+
+@property(nonatomic, readwrite, strong) FIRMessagingCheckinService *checkinService;
+@property(nonatomic, readwrite, strong) FIRMessagingPubSubRegistrar *pubsubRegistrar;
+
+@end
+
+@implementation FIRMessagingRegistrar
+
+- (NSString *)deviceAuthID {
+ return self.checkinService.deviceAuthID;
+}
+
+- (NSString *)secretToken {
+ return self.checkinService.secretToken;
+}
+
+- (instancetype)init {
+ self = [super init];
+ if (self) {
+ _checkinService = [[FIRMessagingCheckinService alloc] init];
+ _pubsubRegistrar = [[FIRMessagingPubSubRegistrar alloc] initWithCheckinService:_checkinService];
+ }
+ return self;
+}
+
+#pragma mark - Checkin
+
+- (BOOL)tryToLoadValidCheckinInfo {
+ if (![self.checkinService hasValidCheckinInfo]) {
+ [self.checkinService tryToLoadPrefetchedCheckinPreferences];
+ }
+ return [self.checkinService hasValidCheckinInfo];
+}
+
+- (BOOL)hasValidCheckinInfo {
+ return [self.checkinService hasValidCheckinInfo];
+}
+
+#pragma mark - Subscribe/Unsubscribe
+
+- (void)updateSubscriptionToTopic:(NSString *)topic
+ withToken:(NSString *)token
+ options:(NSDictionary *)options
+ shouldDelete:(BOOL)shouldDelete
+ handler:(FIRMessagingTopicOperationCompletion)handler {
+ _FIRMessagingDevAssert(handler, @"Invalid nil handler");
+
+ if ([self tryToLoadValidCheckinInfo]) {
+ [self doUpdateSubscriptionForTopic:topic
+ token:token
+ options:options
+ shouldDelete:shouldDelete
+ completion:handler];
+
+ } else {
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeRegistrar000,
+ @"Device check in error, no auth credentials found");
+ NSError *error = [NSError errorWithFCMErrorCode:kFIRMessagingErrorCodeMissingDeviceID];
+ handler(FIRMessagingTopicOperationResultError, error);
+ }
+}
+
+- (void)cancelAllRequests {
+ self.stopAllSubscriptions = YES;
+ [self.pubsubRegistrar stopAllSubscriptionRequests];
+}
+
+#pragma mark - Private
+
+- (void)doUpdateSubscriptionForTopic:(NSString *)topic
+ token:(NSString *)token
+ options:(NSDictionary *)options
+ shouldDelete:(BOOL)shouldDelete
+ completion:(FIRMessagingTopicOperationCompletion)completion {
+ _FIRMessagingDevAssert([self.checkinService hasValidCheckinInfo],
+ @"No valid checkin info found before subscribe");
+
+ [self.pubsubRegistrar updateSubscriptionToTopic:topic
+ withToken:token
+ options:options
+ shouldDelete:shouldDelete
+ handler:completion];
+}
+
+@end
diff --git a/Firebase/Messaging/FIRMessagingRemoteNotificationsProxy.h b/Firebase/Messaging/FIRMessagingRemoteNotificationsProxy.h
new file mode 100644
index 0000000..59c3c15
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingRemoteNotificationsProxy.h
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+/**
+ * Swizzle remote-notification callbacks to invoke FIRMessaging methods
+ * before calling original implementations.
+ */
+@interface FIRMessagingRemoteNotificationsProxy : NSObject
+
+/**
+ * Checks the `FirebaseAppDelegateProxyEnabled` key in the App's Info.plist. If the key is
+ * missing or incorrectly formatted, returns `YES`.
+ *
+ * @return YES if the Application Delegate and User Notification Center methods can be swizzled.
+ * Otherwise, returns NO.
+ */
++ (BOOL)canSwizzleMethods;
+
+/**
+ * Swizzles Application Delegate's remote-notification callbacks and User Notification Center
+ * delegate callback, and invokes the original selectors once done.
+ */
++ (void)swizzleMethods;
+
+@end
diff --git a/Firebase/Messaging/FIRMessagingRemoteNotificationsProxy.m b/Firebase/Messaging/FIRMessagingRemoteNotificationsProxy.m
new file mode 100644
index 0000000..5432c79
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingRemoteNotificationsProxy.m
@@ -0,0 +1,613 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRMessagingRemoteNotificationsProxy.h"
+
+#import <objc/runtime.h>
+#import <UIKit/UIKit.h>
+
+#import "FIRMessagingConstants.h"
+#import "FIRMessagingLogger.h"
+#import "FIRMessaging_Private.h"
+
+static const BOOL kDefaultAutoRegisterEnabledValue = YES;
+static void * UserNotificationObserverContext = &UserNotificationObserverContext;
+
+static NSString *kUserNotificationWillPresentSelectorString =
+ @"userNotificationCenter:willPresentNotification:withCompletionHandler:";
+
+@interface FIRMessagingRemoteNotificationsProxy ()
+
+@property(strong, nonatomic) NSMutableDictionary<NSString *, NSValue *> *originalAppDelegateImps;
+@property(strong, nonatomic) NSMutableDictionary<NSString *, NSArray *> *swizzledSelectorsByClass;
+
+@property(nonatomic) BOOL didSwizzleMethods;
+@property(nonatomic) BOOL didSwizzleAppDelegateMethods;
+
+@property(nonatomic) BOOL hasSwizzledUserNotificationDelegate;
+@property(nonatomic) BOOL isObservingUserNotificationDelegateChanges;
+
+@property(strong, nonatomic) id userNotificationCenter;
+@property(strong, nonatomic) id currentUserNotificationCenterDelegate;
+
+@end
+
+@implementation FIRMessagingRemoteNotificationsProxy
+
++ (BOOL)canSwizzleMethods {
+ id canSwizzleValue =
+ [[NSBundle mainBundle]
+ objectForInfoDictionaryKey: kFIRMessagingRemoteNotificationsProxyEnabledInfoPlistKey];
+ if (canSwizzleValue && [canSwizzleValue isKindOfClass:[NSNumber class]]) {
+ NSNumber *canSwizzleNumberValue = (NSNumber *)canSwizzleValue;
+ return canSwizzleNumberValue.boolValue;
+ } else {
+ return kDefaultAutoRegisterEnabledValue;
+ }
+}
+
++ (void)swizzleMethods {
+ [[FIRMessagingRemoteNotificationsProxy sharedProxy] swizzleMethodsIfPossible];
+}
+
++ (instancetype)sharedProxy {
+ static FIRMessagingRemoteNotificationsProxy *proxy;
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ proxy = [[FIRMessagingRemoteNotificationsProxy alloc] init];
+ });
+ return proxy;
+}
+
+- (instancetype)init {
+ self = [super init];
+ if (self) {
+ _originalAppDelegateImps = [[NSMutableDictionary alloc] init];
+ _swizzledSelectorsByClass = [[NSMutableDictionary alloc] init];
+ }
+ return self;
+}
+
+- (void)dealloc {
+ [self unswizzleAllMethods];
+ self.swizzledSelectorsByClass = nil;
+ [self.originalAppDelegateImps removeAllObjects];
+ self.originalAppDelegateImps = nil;
+ [self removeUserNotificationCenterDelegateObserver];
+}
+
+- (void)swizzleMethodsIfPossible {
+ // Already swizzled.
+ if (self.didSwizzleMethods) {
+ return;
+ }
+
+ NSObject<UIApplicationDelegate> *appDelegate = [[UIApplication sharedApplication] delegate];
+ [self swizzleAppDelegateMethods:appDelegate];
+
+ // Add KVO listener on [UNUserNotificationCenter currentNotificationCenter]'s delegate property
+ Class notificationCenterClass = NSClassFromString(@"UNUserNotificationCenter");
+ if (notificationCenterClass) {
+ // We are linked against iOS 10 SDK or above
+ id notificationCenter = getNamedPropertyFromObject(notificationCenterClass,
+ @"currentNotificationCenter",
+ notificationCenterClass);
+ if (notificationCenter) {
+ [self listenForDelegateChangesInUserNotificationCenter:notificationCenter];
+ }
+ }
+
+ self.didSwizzleMethods = YES;
+}
+
+- (void)unswizzleAllMethods {
+ for (NSString *className in self.swizzledSelectorsByClass) {
+ Class klass = NSClassFromString(className);
+ NSArray *selectorStrings = self.swizzledSelectorsByClass[className];
+ for (NSString *selectorString in selectorStrings) {
+ SEL selector = NSSelectorFromString(selectorString);
+ [self unswizzleSelector:selector inClass:klass];
+ }
+ }
+ [self.swizzledSelectorsByClass removeAllObjects];
+}
+
+- (void)swizzleAppDelegateMethods:(id<UIApplicationDelegate>)appDelegate {
+ if (![appDelegate conformsToProtocol:@protocol(UIApplicationDelegate)]) {
+ return;
+ }
+ Class appDelegateClass = [appDelegate class];
+
+ BOOL didSwizzleAppDelegate = NO;
+ // Message receiving handler for iOS 9, 8, 7 devices (both display notification and data message).
+ SEL remoteNotificationSelector =
+ @selector(application:didReceiveRemoteNotification:);
+
+ SEL remoteNotificationWithFetchHandlerSelector =
+ @selector(application:didReceiveRemoteNotification:fetchCompletionHandler:);
+
+ // For data message from MCS.
+ SEL receiveDataMessageSelector = NSSelectorFromString(@"applicationReceivedRemoteMessage:");
+
+ // For recording when APNS tokens are registered (or fail to register)
+ SEL registerForAPNSFailSelector =
+ @selector(application:didFailToRegisterForRemoteNotificationsWithError:);
+
+ SEL registerForAPNSSuccessSelector =
+ @selector(application:didRegisterForRemoteNotificationsWithDeviceToken:);
+
+
+ // Receive Remote Notifications.
+ BOOL selectorWithFetchHandlerImplemented = NO;
+ if ([appDelegate respondsToSelector:remoteNotificationWithFetchHandlerSelector]) {
+ selectorWithFetchHandlerImplemented = YES;
+ [self swizzleSelector:remoteNotificationWithFetchHandlerSelector
+ inClass:appDelegateClass
+ withImplementation:(IMP)FCM_swizzle_appDidReceiveRemoteNotificationWithHandler
+ inProtocol:@protocol(UIApplicationDelegate)];
+ didSwizzleAppDelegate = YES;
+ }
+
+ if ([appDelegate respondsToSelector:remoteNotificationSelector] ||
+ !selectorWithFetchHandlerImplemented) {
+ [self swizzleSelector:remoteNotificationSelector
+ inClass:appDelegateClass
+ withImplementation:(IMP)FCM_swizzle_appDidReceiveRemoteNotification
+ inProtocol:@protocol(UIApplicationDelegate)];
+ didSwizzleAppDelegate = YES;
+ }
+
+ if ([appDelegate respondsToSelector:receiveDataMessageSelector]) {
+ [self swizzleSelector:receiveDataMessageSelector
+ inClass:appDelegateClass
+ withImplementation:(IMP)FCM_swizzle_applicationReceivedRemoteMessage
+ inProtocol:@protocol(UIApplicationDelegate)];
+ didSwizzleAppDelegate = YES;
+ }
+
+ // Receive APNS token
+ [self swizzleSelector:registerForAPNSSuccessSelector
+ inClass:appDelegateClass
+ withImplementation:(IMP)FCM_swizzle_appDidRegisterForRemoteNotifications
+ inProtocol:@protocol(UIApplicationDelegate)];
+
+ [self swizzleSelector:registerForAPNSFailSelector
+ inClass:appDelegateClass
+ withImplementation:(IMP)FCM_swizzle_appDidFailToRegisterForRemoteNotifications
+ inProtocol:@protocol(UIApplicationDelegate)];
+
+ self.didSwizzleAppDelegateMethods = didSwizzleAppDelegate;
+}
+
+- (void)listenForDelegateChangesInUserNotificationCenter:(id)notificationCenter {
+ Class notificationCenterClass = NSClassFromString(@"UNUserNotificationCenter");
+ if (![notificationCenter isKindOfClass:notificationCenterClass]) {
+ return;
+ }
+ id delegate = getNamedPropertyFromObject(notificationCenter, @"delegate", nil);
+ Protocol *delegateProtocol = NSProtocolFromString(@"UNUserNotificationCenterDelegate");
+ if ([delegate conformsToProtocol:delegateProtocol]) {
+ // Swizzle this object now, if available
+ [self swizzleUserNotificationCenterDelegate:delegate];
+ }
+ // Add KVO observer for "delegate" keyPath for future changes
+ [self addDelegateObserverToUserNotificationCenter:notificationCenter];
+}
+
+#pragma mark - UNNotificationCenter Swizzling
+
+- (void)swizzleUserNotificationCenterDelegate:(id)delegate {
+ if (self.currentUserNotificationCenterDelegate == delegate) {
+ // Via pointer-check, compare if we have already swizzled this item.
+ return;
+ }
+ Protocol *userNotificationCenterProtocol =
+ NSProtocolFromString(@"UNUserNotificationCenterDelegate");
+ if ([delegate conformsToProtocol:userNotificationCenterProtocol]) {
+ SEL willPresentNotificationSelector =
+ NSSelectorFromString(kUserNotificationWillPresentSelectorString);
+ [self swizzleSelector:willPresentNotificationSelector
+ inClass:[delegate class]
+ withImplementation:(IMP)FCM_swizzle_willPresentNotificationWithHandler
+ inProtocol:userNotificationCenterProtocol];
+ self.currentUserNotificationCenterDelegate = delegate;
+ self.hasSwizzledUserNotificationDelegate = YES;
+ }
+}
+
+- (void)unswizzleUserNotificationCenterDelegate:(id)delegate {
+ if (self.currentUserNotificationCenterDelegate != delegate) {
+ // We aren't swizzling this delegate, so don't do anything.
+ return;
+ }
+ SEL willPresentNotificationSelector =
+ NSSelectorFromString(kUserNotificationWillPresentSelectorString);
+ [self unswizzleSelector:willPresentNotificationSelector
+ inClass:[self.currentUserNotificationCenterDelegate class]];
+ self.currentUserNotificationCenterDelegate = nil;
+ self.hasSwizzledUserNotificationDelegate = NO;
+}
+
+#pragma mark - KVO for UNUserNotificationCenter
+
+- (void)addDelegateObserverToUserNotificationCenter:(id)userNotificationCenter {
+ [self removeUserNotificationCenterDelegateObserver];
+ @try {
+ [userNotificationCenter addObserver:self
+ forKeyPath:NSStringFromSelector(@selector(delegate))
+ options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
+ context:UserNotificationObserverContext];
+ self.userNotificationCenter = userNotificationCenter;
+ self.isObservingUserNotificationDelegateChanges = YES;
+ } @catch (NSException *exception) {
+ FIRMessagingLoggerError(kFIRMessagingMessageCodeRemoteNotificationsProxy000,
+ @"Encountered exception trying to add a KVO observer for "
+ @"UNUserNotificationCenter's 'delegate' property: %@",
+ exception);
+ } @finally {
+
+ }
+}
+
+- (void)removeUserNotificationCenterDelegateObserver {
+ if (!self.userNotificationCenter) {
+ return;
+ }
+ @try {
+ [self.userNotificationCenter removeObserver:self
+ forKeyPath:NSStringFromSelector(@selector(delegate))
+ context:UserNotificationObserverContext];
+ self.userNotificationCenter = nil;
+ self.isObservingUserNotificationDelegateChanges = NO;
+ } @catch (NSException *exception) {
+ FIRMessagingLoggerError(kFIRMessagingMessageCodeRemoteNotificationsProxy001,
+ @"Encountered exception trying to remove a KVO observer for "
+ @"UNUserNotificationCenter's 'delegate' property: %@",
+ exception);
+ } @finally {
+
+ }
+}
+
+- (void)observeValueForKeyPath:(NSString *)keyPath
+ ofObject:(id)object
+ change:(NSDictionary<NSKeyValueChangeKey, id> *)change
+ context:(void *)context {
+ if (context == UserNotificationObserverContext) {
+ if ([keyPath isEqualToString:NSStringFromSelector(@selector(delegate))]) {
+ id oldDelegate = change[NSKeyValueChangeOldKey];
+ if (oldDelegate && oldDelegate != [NSNull null]) {
+ [self unswizzleUserNotificationCenterDelegate:oldDelegate];
+ }
+ id newDelegate = change[NSKeyValueChangeNewKey];
+ if (newDelegate && newDelegate != [NSNull null]) {
+ [self swizzleUserNotificationCenterDelegate:newDelegate];
+ }
+ }
+ } else {
+ [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
+ }
+}
+
+#pragma mark - NSProxy methods
+
+- (void)saveOriginalImplementation:(IMP)imp forSelector:(SEL)selector {
+ if (imp && selector) {
+ NSValue *IMPValue = [NSValue valueWithPointer:imp];
+ NSString *selectorString = NSStringFromSelector(selector);
+ self.originalAppDelegateImps[selectorString] = IMPValue;
+ }
+}
+
+- (IMP)originalImplementationForSelector:(SEL)selector {
+ NSString *selectorString = NSStringFromSelector(selector);
+ NSValue *implementation_value = self.originalAppDelegateImps[selectorString];
+ if (!implementation_value) {
+ return nil;
+ }
+
+ IMP imp;
+ [implementation_value getValue:&imp];
+ return imp;
+}
+
+- (void)trackSwizzledSelector:(SEL)selector ofClass:(Class)klass {
+ NSString *className = NSStringFromClass(klass);
+ NSString *selectorString = NSStringFromSelector(selector);
+ NSArray *selectors = self.swizzledSelectorsByClass[selectorString];
+ if (selectors) {
+ selectors = [selectors arrayByAddingObject:selectorString];
+ } else {
+ selectors = @[selectorString];
+ }
+ self.swizzledSelectorsByClass[className] = selectors;
+}
+
+- (void)removeImplementationForSelector:(SEL)selector {
+ NSString *selectorString = NSStringFromSelector(selector);
+ [self.originalAppDelegateImps removeObjectForKey:selectorString];
+}
+
+- (void)swizzleSelector:(SEL)originalSelector
+ inClass:(Class)klass
+ withImplementation:(IMP)swizzledImplementation
+ inProtocol:(Protocol *)protocol {
+ Method originalMethod = class_getInstanceMethod(klass, originalSelector);
+
+ if (originalMethod) {
+ // This class implements this method, so replace the original implementation
+ // with our new implementation and save the old implementation.
+
+ IMP __original_method_implementation =
+ method_setImplementation(originalMethod, swizzledImplementation);
+
+ IMP __nonexistant_method_implementation = [self nonExistantMethodImplementationForClass:klass];
+
+ if (__original_method_implementation &&
+ __original_method_implementation != __nonexistant_method_implementation &&
+ __original_method_implementation != swizzledImplementation) {
+ [self saveOriginalImplementation:__original_method_implementation
+ forSelector:originalSelector];
+ }
+ } else {
+ // The class doesn't have this method, so add our swizzled implementation as the
+ // original implementation of the original method.
+ struct objc_method_description method_description =
+ protocol_getMethodDescription(protocol, originalSelector, NO, YES);
+
+ class_addMethod(klass,
+ originalSelector,
+ swizzledImplementation,
+ method_description.types);
+ }
+ [self trackSwizzledSelector:originalSelector ofClass:klass];
+}
+
+- (void)unswizzleSelector:(SEL)selector inClass:(Class)klass {
+
+ Method swizzledMethod = class_getInstanceMethod(klass, selector);
+ if (!swizzledMethod) {
+ // This class doesn't seem to have this selector as an instance method? Bail out.
+ return;
+ }
+
+ IMP original_imp = [self originalImplementationForSelector:selector];
+ if (original_imp) {
+ // Restore the original implementation as the current implementation
+ method_setImplementation(swizzledMethod, original_imp);
+ [self removeImplementationForSelector:selector];
+ } else {
+ // This class originally did not have an implementation for this selector.
+
+ // We can't actually remove methods in Objective C 2.0, but we could set
+ // its method to something non-existent. This should give us the same
+ // behavior as if the method was not implemented.
+ // See: http://stackoverflow.com/a/8276527/9849
+
+ IMP nonExistantMethodImplementation = [self nonExistantMethodImplementationForClass:klass];
+ method_setImplementation(swizzledMethod, nonExistantMethodImplementation);
+ }
+}
+
+#pragma mark - Reflection Helpers
+
+// This is useful to generate from a stable, "known missing" selector, as the IMP can be compared
+// in case we are setting an implementation for a class that was previously "unswizzled" into a
+// non-existant implementation.
+- (IMP)nonExistantMethodImplementationForClass:(Class)klass {
+ SEL nonExistantSelector = NSSelectorFromString(@"aNonExistantMethod");
+ IMP nonExistantMethodImplementation = class_getMethodImplementation(klass, nonExistantSelector);
+ return nonExistantMethodImplementation;
+}
+
+// A safe, non-leaky way return a property object by its name
+id getNamedPropertyFromObject(id object, NSString *propertyName, Class klass) {
+ SEL selector = NSSelectorFromString(propertyName);
+ if (![object respondsToSelector:selector]) {
+ return nil;
+ }
+ if (!klass) {
+ klass = [NSObject class];
+ }
+ // Suppress clang warning about leaks in performSelector
+ // The alternative way to perform this is to invoke
+ // the method as a block (see http://stackoverflow.com/a/20058585),
+ // but this approach sometimes returns incomplete objects.
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
+ id property = [object performSelector:selector];
+#pragma clang diagnostic pop
+ if (![property isKindOfClass:klass]) {
+ return nil;
+ }
+ return property;
+}
+
+#pragma mark - Swizzled Methods
+
+void FCM_swizzle_appDidReceiveRemoteNotification(id self,
+ SEL _cmd,
+ UIApplication *app,
+ NSDictionary *userInfo) {
+ [[FIRMessaging messaging] appDidReceiveMessage:userInfo];
+
+ IMP original_imp =
+ [[FIRMessagingRemoteNotificationsProxy sharedProxy] originalImplementationForSelector:_cmd];
+ if (original_imp) {
+ ((void (*)(id, SEL, UIApplication *, NSDictionary *))original_imp)(self,
+ _cmd,
+ app,
+ userInfo);
+ }
+}
+
+void FCM_swizzle_appDidReceiveRemoteNotificationWithHandler(
+ id self, SEL _cmd, UIApplication *app, NSDictionary *userInfo,
+ void (^handler)(UIBackgroundFetchResult)) {
+
+ [[FIRMessaging messaging] appDidReceiveMessage:userInfo];
+
+ IMP original_imp =
+ [[FIRMessagingRemoteNotificationsProxy sharedProxy] originalImplementationForSelector:_cmd];
+ if (original_imp) {
+ ((void (*)(id, SEL, UIApplication *, NSDictionary *,
+ void (^)(UIBackgroundFetchResult)))original_imp)(
+ self, _cmd, app, userInfo, handler);
+ }
+}
+
+/**
+ * Swizzle the notification handler for iOS 10+ devices.
+ * Signature of original handler is as below:
+ * - (void)userNotificationCenter:(UNUserNotificationCenter *)center
+ * willPresentNotification:(UNNotification *)notification
+ * withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler
+ * In order to make FCM SDK compile and compatible with iOS SDKs before iOS 10, hide the
+ * parameter types from the swizzling implementation.
+ */
+void FCM_swizzle_willPresentNotificationWithHandler(
+ id self, SEL _cmd, id center, id notification, void (^handler)(NSUInteger)) {
+
+ FIRMessagingRemoteNotificationsProxy *proxy = [FIRMessagingRemoteNotificationsProxy sharedProxy];
+ IMP original_imp = [proxy originalImplementationForSelector:_cmd];
+
+ void (^callOriginalMethodIfAvailable)() = ^{
+ if (original_imp) {
+ ((void (*)(id, SEL, id, id, void (^)(NSUInteger)))original_imp)(
+ self, _cmd, center, notification, handler);
+ }
+ return;
+ };
+
+ Class notificationCenterClass = NSClassFromString(@"UNUserNotificationCenter");
+ Class notificationClass = NSClassFromString(@"UNNotification");
+ if (!notificationCenterClass || !notificationClass) {
+ // Can't find UserNotifications framework. Do not swizzle, just execute the original method.
+ callOriginalMethodIfAvailable();
+ }
+
+ if (!center || ![center isKindOfClass:[notificationCenterClass class]]) {
+ // Invalid parameter type from the original method.
+ // Do not swizzle, just execute the original method.
+ callOriginalMethodIfAvailable();
+ return;
+ }
+
+ if (!notification || ![notification isKindOfClass:[notificationClass class]]) {
+ // Invalid parameter type from the original method.
+ // Do not swizzle, just execute the original method.
+ callOriginalMethodIfAvailable();
+ return;
+ }
+
+ if (!handler) {
+ // Invalid parameter type from the original method.
+ // Do not swizzle, just execute the original method.
+ callOriginalMethodIfAvailable();
+ return;
+ }
+
+ // Valid original method signature, go ahead to swizzle.
+ // Select the userInfo field from UNNotification.request.content.userInfo.
+ SEL requestSelector = NSSelectorFromString(@"request");
+ if (![notification respondsToSelector:requestSelector]) {
+ // This is not the expected notification handler. Do not swizzle, just execute the original
+ // method.
+ callOriginalMethodIfAvailable();
+ return;
+ }
+ Class requestClass = NSClassFromString(@"UNNotificationRequest");
+ id notificationRequest = getNamedPropertyFromObject(notification, @"request", requestClass);
+
+ SEL notificationContentSelector = NSSelectorFromString(@"content");
+ if (!notificationRequest
+ || ![notificationRequest respondsToSelector:notificationContentSelector]) {
+ // This is not the expected notification handler. Do not swizzle, just execute the original
+ // method.
+ callOriginalMethodIfAvailable();
+ return;
+ }
+ Class contentClass = NSClassFromString(@"UNNotificationContent");
+ id notificationContent = getNamedPropertyFromObject(notificationRequest,
+ @"content",
+ contentClass);
+
+ SEL notificationUserInfoSelector = NSSelectorFromString(@"userInfo");
+ if (!notificationContent
+ || ![notificationContent respondsToSelector:notificationUserInfoSelector]) {
+ // This is not the expected notification handler. Do not swizzle, just execute the original
+ // method.
+ callOriginalMethodIfAvailable();
+ return;
+ }
+ id notificationUserInfo = getNamedPropertyFromObject(notificationContent,
+ @"userInfo",
+ [NSDictionary class]);
+
+ if (!notificationUserInfo) {
+ // This is not the expected notification handler. Do not swizzle, just execute the original
+ // method.
+ callOriginalMethodIfAvailable();
+ return;
+ }
+
+ [[FIRMessaging messaging] appDidReceiveMessage:notificationUserInfo];
+ // Execute the original implementation.
+ callOriginalMethodIfAvailable();
+}
+
+void FCM_swizzle_applicationReceivedRemoteMessage(
+ id self, SEL _cmd, FIRMessagingRemoteMessage *remoteMessage) {
+ [[FIRMessaging messaging] appDidReceiveMessage:remoteMessage.appData];
+
+ IMP original_imp =
+ [[FIRMessagingRemoteNotificationsProxy sharedProxy] originalImplementationForSelector:_cmd];
+ if (original_imp) {
+ ((void (*)(id, SEL, FIRMessagingRemoteMessage *))original_imp)(self, _cmd, remoteMessage);
+ }
+}
+
+void FCM_swizzle_appDidFailToRegisterForRemoteNotifications(id self,
+ SEL _cmd,
+ UIApplication *app,
+ NSError *error) {
+ // Log the fact that we failed to register for remote notifications
+ FIRMessagingLoggerError(kFIRMessagingMessageCodeRemoteNotificationsProxyAPNSFailed,
+ @"Error in "
+ @"application:didFailToRegisterForRemoteNotificationsWithError: %@",
+ error.localizedDescription);
+ IMP original_imp =
+ [[FIRMessagingRemoteNotificationsProxy sharedProxy] originalImplementationForSelector:_cmd];
+ if (original_imp) {
+ ((void (*)(id, SEL, UIApplication *, NSError *))original_imp)(self, _cmd, app, error);
+ }
+}
+
+void FCM_swizzle_appDidRegisterForRemoteNotifications(id self,
+ SEL _cmd,
+ UIApplication *app,
+ NSData *deviceToken) {
+ // Pass the APNSToken along to FIRMessaging (and auto-detect the token type)
+ [FIRMessaging messaging].APNSToken = deviceToken;
+
+ IMP original_imp =
+ [[FIRMessagingRemoteNotificationsProxy sharedProxy] originalImplementationForSelector:_cmd];
+ if (original_imp) {
+ ((void (*)(id, SEL, UIApplication *, NSData *))original_imp)(self, _cmd, app, deviceToken);
+ }
+}
+
+@end
diff --git a/Firebase/Messaging/FIRMessagingRmq2PersistentStore.h b/Firebase/Messaging/FIRMessagingRmq2PersistentStore.h
new file mode 100644
index 0000000..09f1d44
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingRmq2PersistentStore.h
@@ -0,0 +1,201 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+@class FIRMessagingPersistentSyncMessage;
+
+// table data handlers
+/**
+ * Handle message stored in the outgoing RMQ messages table.
+ *
+ * @param rmqId The rmqID of the message.
+ * @param tag The message tag.
+ * @param data The data stored in the message.
+ */
+typedef void(^FCMOutgoingRmqMessagesTableHandler)(int64_t rmqId, int8_t tag, NSData *data);
+
+/// Outgoing messages RMQ table
+extern NSString *const kTableOutgoingRmqMessages;
+/// Server to device RMQ table
+extern NSString *const kTableS2DRmqIds;
+
+@interface FIRMessagingRmq2PersistentStore : NSObject
+
+/**
+ * Initialize and open the RMQ database on the client.
+ *
+ * @param databaseName The name for RMQ database.
+ *
+ * @return A store used to persist messages on the client.
+ */
+- (instancetype)initWithDatabaseName:(NSString *)databaseName;
+
+/**
+ * Save outgoing message in RMQ.
+ *
+ * @param rmqId The rmqID for the message.
+ * @param tag The tag of the message proto.
+ * @param data The data being sent in the message.
+ * @param error The error if any while saving the message to the persistent store.
+ *
+ * @return YES if the message was successfully saved to the persistent store else NO.
+ */
+- (BOOL)saveMessageWithRmqId:(int64_t)rmqId
+ tag:(int8_t)tag
+ data:(NSData *)data
+ error:(NSError **)error;
+
+/**
+ * Add unacked server to device message with a given rmqID to the persistent store.
+ *
+ * @param rmqId The rmqID of the message that was not acked by the cient.
+ *
+ * @return YES if the save was successful else NO.
+ */
+- (BOOL)saveUnackedS2dMessageWithRmqId:(NSString *)rmqId;
+
+/**
+ * Update the last RMQ ID that was sent by the client.
+ *
+ * @param rmqID The latest rmqID sent by the device.
+ *
+ * @return YES if the last rmqID was successfully saved else NO.
+ */
+- (BOOL)updateLastOutgoingRmqId:(int64_t)rmqID;
+
+#pragma mark - Query
+
+/**
+ * Query the highest rmqID saved in the Outgoing messages table.
+ *
+ * @return The highest rmqID amongst all the messages in the Outgoing RMQ table. If no message
+ * was ever persisted return 0.
+ */
+- (int64_t)queryHighestRmqId;
+
+/**
+ * The last rmqID that was saved on the client.
+ *
+ * @return The last rmqID that was saved. If no rmqID was ever persisted return 0.
+ */
+- (int64_t)queryLastRmqId;
+
+/**
+ * Get a list of all unacked server to device messages stored on the client.
+ *
+ * @return List of all unacked s2d messages in the persistent store.
+ */
+- (NSArray *)unackedS2dRmqIds;
+
+/**
+ * Iterate over all outgoing messages in the RMQ table.
+ *
+ * @param handler The handler invoked with each message in the outgoing RMQ table.
+ */
+- (void)scanOutgoingRmqMessagesWithHandler:(FCMOutgoingRmqMessagesTableHandler)handler;
+
+#pragma mark - Delete
+
+/**
+ * Delete messages with given rmqID's from a table.
+ *
+ * @param tableName The table name from which to delete the rmq messages.
+ * @param rmqIds The rmqID's of the messages to be deleted.
+ *
+ * @return The number of messages that were successfully deleted.
+ */
+- (int)deleteMessagesFromTable:(NSString *)tableName
+ withRmqIds:(NSArray *)rmqIds;
+
+/**
+ * Remove database from the device.
+ *
+ * @param dbName The database name to be deleted.
+ */
++ (void)removeDatabase:(NSString *)dbName;
+
+#pragma mark - Sync Messages
+
+/**
+ * Save sync message to persistent store to check for duplicates.
+ *
+ * @param rmqID The rmqID of the message to save.
+ * @param expirationTime The expiration time of the message to save.
+ * @param apnsReceived YES if the message was received via APNS else NO.
+ * @param mcsReceived YES if the message was received via MCS else NO.
+ * @param error The error if any while saving the message to store.
+ *
+ * @return YES if the message was saved successfully else NO.
+ */
+- (BOOL)saveSyncMessageWithRmqID:(NSString *)rmqID
+ expirationTime:(int64_t)expirationTime
+ apnsReceived:(BOOL)apnsReceived
+ mcsReceived:(BOOL)mcsReceived
+ error:(NSError **)error;
+
+/**
+ * Update sync message received via APNS.
+ *
+ * @param rmqID The rmqID of the sync message.
+ * @param error The error if any while updating the sync message in persistence.
+ *
+ * @return YES if the update was successful else NO.
+ */
+- (BOOL)updateSyncMessageViaAPNSWithRmqID:(NSString *)rmqID
+ error:(NSError **)error;
+
+/**
+ * Update sync message received via MCS.
+ *
+ * @param rmqID The rmqID of the sync message.
+ * @param error The error if any while updating the sync message in persistence.
+ *
+ * @return YES if the update was successful else NO.
+ */
+- (BOOL)updateSyncMessageViaMCSWithRmqID:(NSString *)rmqID
+ error:(NSError **)error;
+
+/**
+ * Query sync message table for a given rmqID.
+ *
+ * @param rmqID The rmqID to search for in SYNC_RMQ.
+ *
+ * @return The sync message that was persisted with `rmqID`. If no such message was persisted
+ * return nil.
+ */
+- (FIRMessagingPersistentSyncMessage *)querySyncMessageWithRmqID:(NSString *)rmqID;
+
+/**
+ * Delete sync message with rmqID.
+ *
+ * @param rmqID The rmqID of the message to delete.
+ *
+ * @return YES if a sync message with rmqID was found and deleted successfully else NO.
+ */
+- (BOOL)deleteSyncMessageWithRmqID:(NSString *)rmqID;
+
+/**
+ * Delete the expired sync messages from persisten store. Also deletes messages that have been
+ * delivered both via APNS and MCS.
+ *
+ * @param error The error if any while deleting the messages.
+ *
+ * @return The total number of messages that were deleted from the persistent store.
+ */
+- (int)deleteExpiredOrFinishedSyncMessages:(NSError **)error;
+
+@end
diff --git a/Firebase/Messaging/FIRMessagingRmq2PersistentStore.m b/Firebase/Messaging/FIRMessagingRmq2PersistentStore.m
new file mode 100644
index 0000000..9edac40
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingRmq2PersistentStore.m
@@ -0,0 +1,770 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRMessagingRmq2PersistentStore.h"
+
+#import "sqlite3.h"
+
+#import "Protos/GtalkCore.pbobjc.h"
+
+#import "FIRMessagingConstants.h"
+#import "FIRMessagingDefines.h"
+#import "FIRMessagingLogger.h"
+#import "FIRMessagingPersistentSyncMessage.h"
+#import "FIRMessagingUtilities.h"
+#import "NSError+FIRMessaging.h"
+
+#ifndef _FIRMessagingRmqLogAndExit
+#define _FIRMessagingRmqLogAndExit(stmt, return_value) \
+do { \
+[self logErrorAndFinalizeStatement:stmt]; \
+return return_value; \
+} while(0)
+#endif
+
+typedef enum : NSUInteger {
+ FIRMessagingRmqDirectoryUnknown,
+ FIRMessagingRmqDirectoryDocuments,
+ FIRMessagingRmqDirectoryApplicationSupport,
+} FIRMessagingRmqDirectory;
+
+static NSString *const kFCMRmqStoreTag = @"FIRMessagingRmqStore:";
+
+// table names
+NSString *const kTableOutgoingRmqMessages = @"outgoingRmqMessages";
+NSString *const kTableLastRmqId = @"lastrmqid";
+NSString *const kOldTableS2DRmqIds = @"s2dRmqIds";
+NSString *const kTableS2DRmqIds = @"s2dRmqIds_1";
+
+// Used to prevent de-duping of sync messages received both via APNS and MCS.
+NSString *const kTableSyncMessages = @"incomingSyncMessages";
+
+static NSString *const kTablePrefix = @"";
+
+// create tables
+static NSString *const kCreateTableOutgoingRmqMessages =
+ @"create TABLE IF NOT EXISTS %@%@ "
+ @"(_id INTEGER PRIMARY KEY, "
+ @"rmq_id INTEGER, "
+ @"type INTEGER, "
+ @"ts INTEGER, "
+ @"data BLOB)";
+
+static NSString *const kCreateTableLastRmqId =
+ @"create TABLE IF NOT EXISTS %@%@ "
+ @"(_id INTEGER PRIMARY KEY, "
+ @"rmq_id INTEGER)";
+
+static NSString *const kCreateTableS2DRmqIds =
+ @"create TABLE IF NOT EXISTS %@%@ "
+ @"(_id INTEGER PRIMARY KEY, "
+ @"rmq_id TEXT)";
+
+static NSString *const kCreateTableSyncMessages =
+ @"create TABLE IF NOT EXISTS %@%@ "
+ @"(_id INTEGER PRIMARY KEY, "
+ @"rmq_id TEXT, "
+ @"expiration_ts INTEGER, "
+ @"apns_recv INTEGER, "
+ @"mcs_recv INTEGER)";
+
+static NSString *const kDropTableCommand =
+ @"drop TABLE if exists %@%@";
+
+// table infos
+static NSString *const kRmqIdColumn = @"rmq_id";
+static NSString *const kDataColumn = @"data";
+static NSString *const kProtobufTagColumn = @"type";
+static NSString *const kIdColumn = @"_id";
+
+static NSString *const kOutgoingRmqMessagesColumns = @"rmq_id, type, data";
+
+// Sync message columns
+static NSString *const kSyncMessagesColumns = @"rmq_id, expiration_ts, apns_recv, mcs_recv";
+// Message time expiration in seconds since 1970
+static NSString *const kSyncMessageExpirationTimestampColumn = @"expiration_ts";
+static NSString *const kSyncMessageAPNSReceivedColumn = @"apns_recv";
+static NSString *const kSyncMessageMCSReceivedColumn = @"mcs_recv";
+
+// table data handlers
+typedef void(^FCMOutgoingRmqMessagesTableHandler)(int64_t rmqId, int8_t tag, NSData *data);
+
+@interface FIRMessagingRmq2PersistentStore () {
+ sqlite3 *_database;
+}
+
+@property(nonatomic, readwrite, strong) NSString *databaseName;
+@property(nonatomic, readwrite, assign) FIRMessagingRmqDirectory currentDirectory;
+
+@end
+
+@implementation FIRMessagingRmq2PersistentStore
+
+- (instancetype)initWithDatabaseName:(NSString *)databaseName {
+ self = [super init];
+ if (self) {
+ _databaseName = [databaseName copy];
+ BOOL didMoveToApplicationSupport =
+ [self moveToApplicationSupportSubDirectory:kFIRMessagingApplicationSupportSubDirectory];
+
+ _currentDirectory = didMoveToApplicationSupport
+ ? FIRMessagingRmqDirectoryApplicationSupport
+ : FIRMessagingRmqDirectoryDocuments;
+
+ [self openDatabase:_databaseName];
+ }
+ return self;
+}
+
+- (void)dealloc {
+ sqlite3_close(_database);
+}
+
+- (BOOL)moveToApplicationSupportSubDirectory:(NSString *)subDirectoryName {
+ NSArray *directoryPaths = NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory,
+ NSUserDomainMask, YES);
+ NSString *applicationSupportDirPath = directoryPaths.lastObject;
+ NSArray *components = @[applicationSupportDirPath, subDirectoryName];
+ NSString *subDirectoryPath = [NSString pathWithComponents:components];
+ BOOL hasSubDirectory;
+
+ if (![[NSFileManager defaultManager] fileExistsAtPath:subDirectoryPath
+ isDirectory:&hasSubDirectory]) {
+ // Cannot move to non-existent directory
+ return NO;
+ }
+
+ if ([self doesFileExistInDirectory:FIRMessagingRmqDirectoryDocuments]) {
+ NSString *oldPlistPath = [[self class] pathForDatabase:self.databaseName
+ inDirectory:FIRMessagingRmqDirectoryDocuments];
+ NSString *newPlistPath = [[self class]
+ pathForDatabase:self.databaseName
+ inDirectory:FIRMessagingRmqDirectoryApplicationSupport];
+
+ if ([self doesFileExistInDirectory:FIRMessagingRmqDirectoryApplicationSupport]) {
+ // File exists in both Documents and ApplicationSupport, delete the one in Documents
+ NSError *deleteError;
+ if (![[NSFileManager defaultManager] removeItemAtPath:oldPlistPath error:&deleteError]) {
+ FIRMessagingLoggerError(kFIRMessagingMessageCodeRmq2PersistentStore000,
+ @"Failed to delete old copy of %@.sqlite in Documents %@",
+ self.databaseName, deleteError);
+ }
+ return NO;
+ }
+ NSError *moveError;
+ if (![[NSFileManager defaultManager] moveItemAtPath:oldPlistPath
+ toPath:newPlistPath
+ error:&moveError]) {
+ FIRMessagingLoggerError(kFIRMessagingMessageCodeRmq2PersistentStore001,
+ @"Failed to move file %@ from %@ to %@. Error: %@", self.databaseName,
+ oldPlistPath, newPlistPath, moveError);
+ return NO;
+ }
+ }
+ // We moved the file if it existed, otherwise we didn't need to do anything
+ return YES;
+}
+
+- (BOOL)doesFileExistInDirectory:(FIRMessagingRmqDirectory)directory {
+ NSString *path = [[self class] pathForDatabase:self.databaseName inDirectory:directory];
+ return [[NSFileManager defaultManager] fileExistsAtPath:path];
+}
+
++ (NSString *)pathForDatabase:(NSString *)dbName inDirectory:(FIRMessagingRmqDirectory)directory {
+ NSArray *paths;
+ NSArray *components;
+ NSString *dbNameWithExtension = [NSString stringWithFormat:@"%@.sqlite", dbName];
+
+ switch (directory) {
+ case FIRMessagingRmqDirectoryDocuments:
+ paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
+ components = @[paths.lastObject, dbNameWithExtension];
+ break;
+
+ case FIRMessagingRmqDirectoryApplicationSupport:
+ paths = NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory,
+ NSUserDomainMask,
+ YES);
+ components = @[
+ paths.lastObject,
+ kFIRMessagingApplicationSupportSubDirectory,
+ dbNameWithExtension
+ ];
+ break;
+
+ default:
+ FIRMessaging_FAIL(@"Invalid directory type %zd", directory);
+ break;
+ }
+
+ return [NSString pathWithComponents:components];
+}
+
+- (void)createTableWithName:(NSString *)tableName command:(NSString *)command {
+ char *error;
+ NSString *createDatabase = [NSString stringWithFormat:command, kTablePrefix, tableName];
+ if (sqlite3_exec(_database, [createDatabase UTF8String], NULL, NULL, &error) != SQLITE_OK) {
+ // remove db before failing
+ [self removeDatabase];
+ FIRMessaging_FAIL(@"Couldn't create table: %@ %@",
+ kCreateTableOutgoingRmqMessages,
+ [NSString stringWithCString:error encoding:NSUTF8StringEncoding]);
+ }
+}
+
+- (void)dropTableWithName:(NSString *)tableName {
+ char *error;
+ NSString *dropTableSQL = [NSString stringWithFormat:kDropTableCommand, kTablePrefix, tableName];
+ if (sqlite3_exec(_database, [dropTableSQL UTF8String], NULL, NULL, &error) != SQLITE_OK) {
+ FIRMessagingLoggerError(kFIRMessagingMessageCodeRmq2PersistentStore002,
+ @"Failed to remove table %@", tableName);
+ }
+}
+
+- (void)removeDatabase {
+ NSString *path = [[self class] pathForDatabase:self.databaseName
+ inDirectory:self.currentDirectory];
+ [[NSFileManager defaultManager] removeItemAtPath:path error:nil];
+}
+
++ (void)removeDatabase:(NSString *)dbName {
+ NSString *documentsDirPath = [self pathForDatabase:dbName
+ inDirectory:FIRMessagingRmqDirectoryDocuments];
+ NSString *applicationSupportDirPath =
+ [self pathForDatabase:dbName inDirectory:FIRMessagingRmqDirectoryApplicationSupport];
+ [[NSFileManager defaultManager] removeItemAtPath:documentsDirPath error:nil];
+ [[NSFileManager defaultManager] removeItemAtPath:applicationSupportDirPath error:nil];
+}
+
+- (void)openDatabase:(NSString *)dbName {
+ NSFileManager *fileManager = [NSFileManager defaultManager];
+ NSString *path = [[self class] pathForDatabase:dbName inDirectory:self.currentDirectory];
+
+ BOOL didOpenDatabase = YES;
+ if (![fileManager fileExistsAtPath:path]) {
+ // We've to separate between different versions here because of backwards compatbility issues.
+ if (sqlite3_open([path UTF8String], &_database) != SQLITE_OK) {
+ FIRMessaging_FAIL(@"%@ Could not open rmq database: %@", kFCMRmqStoreTag, path);
+ didOpenDatabase = NO;
+ return;
+ }
+ [self createTableWithName:kTableOutgoingRmqMessages
+ command:kCreateTableOutgoingRmqMessages];
+
+ [self createTableWithName:kTableLastRmqId command:kCreateTableLastRmqId];
+ [self createTableWithName:kTableS2DRmqIds command:kCreateTableS2DRmqIds];
+ } else {
+ if (sqlite3_open([path UTF8String], &_database) != SQLITE_OK) {
+ FIRMessaging_FAIL(@"%@ Could not open rmq database: %@", kFCMRmqStoreTag, path);
+ didOpenDatabase = NO;
+ } else {
+ [self updateDbWithStringRmqID];
+ }
+ }
+
+ if (didOpenDatabase) {
+ [self createTableWithName:kTableSyncMessages command:kCreateTableSyncMessages];
+ }
+}
+
+- (void)updateDbWithStringRmqID {
+ [self createTableWithName:kTableS2DRmqIds command:kCreateTableS2DRmqIds];
+ [self dropTableWithName:kOldTableS2DRmqIds];
+}
+
+#pragma mark - Insert
+
+- (BOOL)saveUnackedS2dMessageWithRmqId:(NSString *)rmqId {
+ NSString *insertFormat = @"INSERT INTO %@ (%@) VALUES (?)";
+ NSString *insertSQL = [NSString stringWithFormat:insertFormat,
+ kTableS2DRmqIds,
+ kRmqIdColumn];
+ sqlite3_stmt *insert_statement;
+ if (sqlite3_prepare_v2(_database, [insertSQL UTF8String], -1, &insert_statement, NULL)
+ != SQLITE_OK) {
+ _FIRMessagingRmqLogAndExit(insert_statement, NO);
+ }
+ if (sqlite3_bind_text(insert_statement,
+ 1,
+ [rmqId UTF8String],
+ (int)[rmqId length],
+ SQLITE_STATIC) != SQLITE_OK) {
+ _FIRMessagingRmqLogAndExit(insert_statement, NO);
+ }
+ if (sqlite3_step(insert_statement) != SQLITE_DONE) {
+ _FIRMessagingRmqLogAndExit(insert_statement, NO);
+ }
+ sqlite3_finalize(insert_statement);
+ return YES;
+}
+
+- (BOOL)saveMessageWithRmqId:(int64_t)rmqId
+ tag:(int8_t)tag
+ data:(NSData *)data
+ error:(NSError **)error {
+ NSString *insertFormat = @"INSERT INTO %@ (%@, %@, %@) VALUES (?, ?, ?)";
+ NSString *insertSQL = [NSString stringWithFormat:insertFormat,
+ kTableOutgoingRmqMessages, // table
+ kRmqIdColumn, kProtobufTagColumn, kDataColumn /* columns */];
+ sqlite3_stmt *insert_statement;
+ if (sqlite3_prepare_v2(_database, [insertSQL UTF8String], -1, &insert_statement, NULL)
+ != SQLITE_OK) {
+ if (error) {
+ *error = [NSError errorWithDomain:[NSString stringWithFormat:@"%s", sqlite3_errmsg(_database)]
+ code:sqlite3_errcode(_database)
+ userInfo:nil];
+ }
+ _FIRMessagingRmqLogAndExit(insert_statement, NO);
+ }
+ if (sqlite3_bind_int64(insert_statement, 1, rmqId) != SQLITE_OK) {
+ _FIRMessagingRmqLogAndExit(insert_statement, NO);
+ }
+ if (sqlite3_bind_int(insert_statement, 2, tag) != SQLITE_OK) {
+ _FIRMessagingRmqLogAndExit(insert_statement, NO);
+ }
+ if (sqlite3_bind_blob(insert_statement, 3, [data bytes], (int)[data length], NULL) != SQLITE_OK) {
+ _FIRMessagingRmqLogAndExit(insert_statement, NO);
+ }
+ if (sqlite3_step(insert_statement) != SQLITE_DONE) {
+ _FIRMessagingRmqLogAndExit(insert_statement, NO);
+ }
+
+ sqlite3_finalize(insert_statement);
+ return YES;
+}
+
+- (int)deleteMessagesFromTable:(NSString *)tableName
+ withRmqIds:(NSArray *)rmqIds {
+ _FIRMessagingDevAssert([tableName isEqualToString:kTableOutgoingRmqMessages] ||
+ [tableName isEqualToString:kTableLastRmqId] ||
+ [tableName isEqualToString:kTableS2DRmqIds] ||
+ [tableName isEqualToString:kTableSyncMessages],
+ @"%@: Invalid Table Name %@", kFCMRmqStoreTag, tableName);
+
+ BOOL isRmqIDString = NO;
+ // RmqID is a string only for outgoing messages
+ if ([tableName isEqualToString:kTableS2DRmqIds] ||
+ [tableName isEqualToString:kTableSyncMessages]) {
+ isRmqIDString = YES;
+ }
+
+ NSMutableString *delete = [NSMutableString stringWithFormat:@"DELETE FROM %@ WHERE ", tableName];
+
+ NSString *toDeleteArgument = [NSString stringWithFormat:@"%@ = ? OR ", kRmqIdColumn];
+
+ int toDelete = (int)[rmqIds count];
+ if (toDelete == 0) {
+ return 0;
+ }
+ int maxBatchSize = 100;
+ int start = 0;
+ int deleteCount = 0;
+ while (start < toDelete) {
+
+ // construct the WHERE argument
+ int end = MIN(start + maxBatchSize, toDelete);
+ NSMutableString *whereArgument = [NSMutableString string];
+ for (int i = start; i < end; i++) {
+ [whereArgument appendString:toDeleteArgument];
+ }
+ // remove the last * OR * from argument
+ NSRange range = NSMakeRange([whereArgument length] -4, 4);
+ [whereArgument deleteCharactersInRange:range];
+ NSString *deleteQuery = [NSString stringWithFormat:@"%@ %@", delete, whereArgument];
+
+
+ // sqlite update
+ sqlite3_stmt *delete_statement;
+ if (sqlite3_prepare_v2(_database, [deleteQuery UTF8String],
+ -1, &delete_statement, NULL) != SQLITE_OK) {
+ _FIRMessagingRmqLogAndExit(delete_statement, 0);
+ }
+
+ // bind values
+ int rmqIndex = 0;
+ int placeholderIndex = 1; // placeholders in sqlite3 start with 1
+ for (NSString *rmqId in rmqIds) { // objectAtIndex: is O(n) -- would make it slow
+ if (rmqIndex < start) {
+ rmqIndex++;
+ continue;
+ } else if (rmqIndex >= end) {
+ break;
+ } else {
+ if (isRmqIDString) {
+ if (sqlite3_bind_text(delete_statement,
+ placeholderIndex,
+ [rmqId UTF8String],
+ (int)[rmqId length],
+ SQLITE_STATIC) != SQLITE_OK) {
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeRmq2PersistentStore003,
+ @"Failed to bind rmqID %@", rmqId);
+ continue;
+ }
+ } else {
+ int64_t rmqIdValue = [rmqId longLongValue];
+ sqlite3_bind_int64(delete_statement, placeholderIndex, rmqIdValue);
+ }
+ placeholderIndex++;
+ }
+ rmqIndex++;
+ }
+ if (sqlite3_step(delete_statement) != SQLITE_DONE) {
+ _FIRMessagingRmqLogAndExit(delete_statement, deleteCount);
+ }
+ sqlite3_finalize(delete_statement);
+ deleteCount += sqlite3_changes(_database);
+ start = end;
+ }
+
+ // if we are here all of our sqlite queries should have succeeded
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeRmq2PersistentStore004,
+ @"%@ Trying to delete %d s2D ID's, successfully deleted %d",
+ kFCMRmqStoreTag, toDelete, deleteCount);
+ return deleteCount;
+}
+
+#pragma mark - Query
+
+- (int64_t)queryHighestRmqId {
+ NSString *queryFormat = @"SELECT %@ FROM %@ ORDER BY %@ DESC LIMIT %d";
+ NSString *query = [NSString stringWithFormat:queryFormat,
+ kRmqIdColumn, // column
+ kTableOutgoingRmqMessages, // table
+ kRmqIdColumn, // order by column
+ 1]; // limit
+
+ sqlite3_stmt *statement;
+ int64_t highestRmqId = 0;
+ if (sqlite3_prepare_v2(_database, [query UTF8String], -1, &statement, NULL) != SQLITE_OK) {
+ _FIRMessagingRmqLogAndExit(statement, highestRmqId);
+ }
+ if (sqlite3_step(statement) == SQLITE_ROW) {
+ highestRmqId = sqlite3_column_int64(statement, 0);
+ }
+ sqlite3_finalize(statement);
+ return highestRmqId;
+}
+
+- (int64_t)queryLastRmqId {
+ NSString *queryFormat = @"SELECT %@ FROM %@ ORDER BY %@ DESC LIMIT %d";
+ NSString *query = [NSString stringWithFormat:queryFormat,
+ kRmqIdColumn, // column
+ kTableLastRmqId, // table
+ kRmqIdColumn, // order by column
+ 1]; // limit
+
+ sqlite3_stmt *statement;
+ int64_t lastRmqId = 0;
+ if (sqlite3_prepare_v2(_database, [query UTF8String], -1, &statement, NULL) != SQLITE_OK) {
+ _FIRMessagingRmqLogAndExit(statement, lastRmqId);
+ }
+ if (sqlite3_step(statement) == SQLITE_ROW) {
+ lastRmqId = sqlite3_column_int64(statement, 0);
+ }
+ sqlite3_finalize(statement);
+ return lastRmqId;
+}
+
+- (BOOL)updateLastOutgoingRmqId:(int64_t)rmqID {
+ NSString *queryFormat = @"INSERT OR REPLACE INTO %@ (%@, %@) VALUES (?, ?)";
+ NSString *query = [NSString stringWithFormat:queryFormat,
+ kTableLastRmqId, // table
+ kIdColumn, kRmqIdColumn]; // columns
+ sqlite3_stmt *statement;
+ if (sqlite3_prepare_v2(_database, [query UTF8String], -1, &statement, NULL) != SQLITE_OK) {
+ _FIRMessagingRmqLogAndExit(statement, NO);
+ }
+ if (sqlite3_bind_int(statement, 1, 1) != SQLITE_OK) {
+ _FIRMessagingRmqLogAndExit(statement, NO);
+ }
+ if (sqlite3_bind_int64(statement, 2, rmqID) != SQLITE_OK) {
+ _FIRMessagingRmqLogAndExit(statement, NO);
+ }
+ if (sqlite3_step(statement) != SQLITE_DONE) {
+ _FIRMessagingRmqLogAndExit(statement, NO);
+ }
+ sqlite3_finalize(statement);
+ return YES;
+}
+
+- (NSArray *)unackedS2dRmqIds {
+ NSString *queryFormat = @"SELECT %@ FROM %@ ORDER BY %@ ASC";
+ NSString *query = [NSString stringWithFormat:queryFormat,
+ kRmqIdColumn,
+ kTableS2DRmqIds,
+ kRmqIdColumn];
+ sqlite3_stmt *statement;
+ if (sqlite3_prepare_v2(_database, [query UTF8String], -1, &statement, NULL) != SQLITE_OK) {
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeRmq2PersistentStore005,
+ @"%@: Could not find s2d ids", kFCMRmqStoreTag);
+ _FIRMessagingRmqLogAndExit(statement, @[]);
+ }
+ NSMutableArray *rmqIDArray = [NSMutableArray array];
+ while (sqlite3_step(statement) == SQLITE_ROW) {
+ const char *rmqID = (char *)sqlite3_column_text(statement, 0);
+ [rmqIDArray addObject:[NSString stringWithUTF8String:rmqID]];
+ }
+ sqlite3_finalize(statement);
+ return rmqIDArray;
+}
+
+#pragma mark - Scan
+
+- (void)scanOutgoingRmqMessagesWithHandler:(FCMOutgoingRmqMessagesTableHandler)handler {
+ static NSString *queryFormat = @"SELECT %@ FROM %@ WHERE %@ != 0 ORDER BY %@ ASC";
+ NSString *query = [NSString stringWithFormat:queryFormat,
+ kOutgoingRmqMessagesColumns, // select (rmq_id, type, data)
+ kTableOutgoingRmqMessages, // from table
+ kRmqIdColumn, // where
+ kRmqIdColumn]; // order by
+ sqlite3_stmt *statement;
+ if (sqlite3_prepare_v2(_database, [query UTF8String], -1, &statement, NULL) != SQLITE_OK) {
+ [self logError];
+ sqlite3_finalize(statement);
+ return;
+ }
+ // can query sqlite3 for this but this is fine
+ const int rmqIdColumnNumber = 0;
+ const int typeColumnNumber = 1;
+ const int dataColumnNumber = 2;
+ while (sqlite3_step(statement) == SQLITE_ROW) {
+ int64_t rmqId = sqlite3_column_int64(statement, rmqIdColumnNumber);
+ int8_t type = sqlite3_column_int(statement, typeColumnNumber);
+ const void *bytes = sqlite3_column_blob(statement, dataColumnNumber);
+ int length = sqlite3_column_bytes(statement, dataColumnNumber);
+ _FIRMessagingDevAssert(bytes != NULL,
+ @"%@ Message with no data being stored in Rmq",
+ kFCMRmqStoreTag);
+ NSData *data = [NSData dataWithBytes:bytes length:length];
+ handler(rmqId, type, data);
+ }
+ sqlite3_finalize(statement);
+}
+
+#pragma mark - Sync Messages
+
+- (FIRMessagingPersistentSyncMessage *)querySyncMessageWithRmqID:(NSString *)rmqID {
+ _FIRMessagingDevAssert([rmqID length], @"Invalid rmqID key %@ to search in SYNC_RMQ", rmqID);
+
+ NSString *queryFormat = @"SELECT %@ FROM %@ WHERE %@ = '%@'";
+ NSString *query = [NSString stringWithFormat:queryFormat,
+ kSyncMessagesColumns, // SELECT (rmq_id, expiration_ts, apns_recv, mcs_recv)
+ kTableSyncMessages, // FROM sync_rmq
+ kRmqIdColumn, // WHERE rmq_id
+ rmqID];
+
+ sqlite3_stmt *stmt;
+ if (sqlite3_prepare_v2(_database, [query UTF8String], -1, &stmt, NULL) != SQLITE_OK) {
+ [self logError];
+ sqlite3_finalize(stmt);
+ return nil;
+ }
+
+ const int rmqIDColumn = 0;
+ const int expirationTimestampColumn = 1;
+ const int apnsReceivedColumn = 2;
+ const int mcsReceivedColumn = 3;
+
+ int count = 0;
+ FIRMessagingPersistentSyncMessage *persistentMessage;
+
+ while (sqlite3_step(stmt) == SQLITE_ROW) {
+ NSString *rmqID =
+ [NSString stringWithUTF8String:(char *)sqlite3_column_text(stmt, rmqIDColumn)];
+ int64_t expirationTimestamp = sqlite3_column_int64(stmt, expirationTimestampColumn);
+ BOOL apnsReceived = sqlite3_column_int(stmt, apnsReceivedColumn);
+ BOOL mcsReceived = sqlite3_column_int(stmt, mcsReceivedColumn);
+
+ // create a new persistent message
+ persistentMessage =
+ [[FIRMessagingPersistentSyncMessage alloc] initWithRMQID:rmqID expirationTime:expirationTimestamp];
+ persistentMessage.apnsReceived = apnsReceived;
+ persistentMessage.mcsReceived = mcsReceived;
+
+ count++;
+ }
+ sqlite3_finalize(stmt);
+
+ _FIRMessagingDevAssert(count <= 1, @"Found multiple messages in %@ with same RMQ ID", kTableSyncMessages);
+ return persistentMessage;
+}
+
+- (BOOL)deleteSyncMessageWithRmqID:(NSString *)rmqID {
+ _FIRMessagingDevAssert([rmqID length], @"Invalid rmqID key %@ to delete in SYNC_RMQ", rmqID);
+ return [self deleteMessagesFromTable:kTableSyncMessages withRmqIds:@[rmqID]] > 0;
+}
+
+- (int)deleteExpiredOrFinishedSyncMessages:(NSError *__autoreleasing *)error {
+ int64_t now = FIRMessagingCurrentTimestampInSeconds();
+ NSString *deleteSQL = @"DELETE FROM %@ "
+ @"WHERE %@ < %lld OR " // expirationTime < now
+ @"(%@ = 1 AND %@ = 1)"; // apns_received = 1 AND mcs_received = 1
+ NSString *query = [NSString stringWithFormat:deleteSQL,
+ kTableSyncMessages,
+ kSyncMessageExpirationTimestampColumn,
+ now,
+ kSyncMessageAPNSReceivedColumn,
+ kSyncMessageMCSReceivedColumn];
+
+ NSString *errorReason = @"Failed to save delete expired sync messages from store.";
+
+ sqlite3_stmt *stmt;
+ if (sqlite3_prepare_v2(_database, [query UTF8String], -1, &stmt, NULL) != SQLITE_OK) {
+ if (error) {
+ *error = [NSError fcm_errorWithCode:sqlite3_errcode(_database)
+ userInfo:@{ @"error" : errorReason }];
+ }
+ _FIRMessagingRmqLogAndExit(stmt, 0);
+ }
+
+ if (sqlite3_step(stmt) != SQLITE_DONE) {
+ if (error) {
+ *error = [NSError fcm_errorWithCode:sqlite3_errcode(_database)
+ userInfo:@{ @"error" : errorReason }];
+ }
+ _FIRMessagingRmqLogAndExit(stmt, 0);
+ }
+
+ sqlite3_finalize(stmt);
+ int deleteCount = sqlite3_changes(_database);
+ return deleteCount;
+}
+
+- (BOOL)saveSyncMessageWithRmqID:(NSString *)rmqID
+ expirationTime:(int64_t)expirationTime
+ apnsReceived:(BOOL)apnsReceived
+ mcsReceived:(BOOL)mcsReceived
+ error:(NSError **)error {
+ _FIRMessagingDevAssert([rmqID length], @"Invalid nil message to persist to SYNC_RMQ");
+
+ NSString *insertFormat = @"INSERT INTO %@ (%@, %@, %@, %@) VALUES (?, ?, ?, ?)";
+ NSString *insertSQL = [NSString stringWithFormat:insertFormat,
+ kTableSyncMessages, // Table name
+ kRmqIdColumn, // rmq_id
+ kSyncMessageExpirationTimestampColumn, // expiration_ts
+ kSyncMessageAPNSReceivedColumn, // apns_recv
+ kSyncMessageMCSReceivedColumn /* mcs_recv */];
+
+ sqlite3_stmt *stmt;
+
+ if (sqlite3_prepare_v2(_database, [insertSQL UTF8String], -1, &stmt, NULL) != SQLITE_OK) {
+ if (error) {
+ *error = [NSError fcm_errorWithCode:sqlite3_errcode(_database)
+ userInfo:@{ @"error" : @"Failed to save sync message to store." }];
+ }
+ _FIRMessagingRmqLogAndExit(stmt, NO);
+ }
+
+ if (sqlite3_bind_text(stmt, 1, [rmqID UTF8String], (int)[rmqID length], NULL) != SQLITE_OK) {
+ _FIRMessagingRmqLogAndExit(stmt, NO);
+ }
+
+ if (sqlite3_bind_int64(stmt, 2, expirationTime) != SQLITE_OK) {
+ _FIRMessagingRmqLogAndExit(stmt, NO);
+ }
+
+ if (sqlite3_bind_int(stmt, 3, apnsReceived ? 1 : 0) != SQLITE_OK) {
+ _FIRMessagingRmqLogAndExit(stmt, NO);
+ }
+
+ if (sqlite3_bind_int(stmt, 4, mcsReceived ? 1 : 0) != SQLITE_OK) {
+ _FIRMessagingRmqLogAndExit(stmt, NO);
+ }
+
+ if (sqlite3_step(stmt) != SQLITE_DONE) {
+ _FIRMessagingRmqLogAndExit(stmt, NO);
+ }
+
+ sqlite3_finalize(stmt);
+ return YES;
+}
+
+- (BOOL)updateSyncMessageViaAPNSWithRmqID:(NSString *)rmqID
+ error:(NSError **)error {
+ return [self updateSyncMessageWithRmqID:rmqID
+ column:kSyncMessageAPNSReceivedColumn
+ value:YES
+ error:error];
+}
+
+- (BOOL)updateSyncMessageViaMCSWithRmqID:(NSString *)rmqID
+ error:(NSError *__autoreleasing *)error {
+ return [self updateSyncMessageWithRmqID:rmqID
+ column:kSyncMessageMCSReceivedColumn
+ value:YES
+ error:error];
+}
+
+- (BOOL)updateSyncMessageWithRmqID:(NSString *)rmqID
+ column:(NSString *)column
+ value:(BOOL)value
+ error:(NSError **)error {
+ _FIRMessagingDevAssert([column isEqualToString:kSyncMessageAPNSReceivedColumn] ||
+ [column isEqualToString:kSyncMessageMCSReceivedColumn],
+ @"Invalid column name %@ for SYNC_RMQ", column);
+ NSString *queryFormat = @"UPDATE %@ " // Table name
+ @"SET %@ = %d " // column=value
+ @"WHERE %@ = ?"; // condition
+ NSString *query = [NSString stringWithFormat:queryFormat,
+ kTableSyncMessages,
+ column,
+ value ? 1 : 0,
+ kRmqIdColumn];
+ sqlite3_stmt *stmt;
+
+ if (sqlite3_prepare_v2(_database, [query UTF8String], -1, &stmt, NULL) != SQLITE_OK) {
+ if (error) {
+ *error = [NSError fcm_errorWithCode:sqlite3_errcode(_database)
+ userInfo:@{ @"error" : @"Failed to update sync message"}];
+ }
+ _FIRMessagingRmqLogAndExit(stmt, NO);
+ }
+
+ if (sqlite3_bind_text(stmt, 1, [rmqID UTF8String], (int)[rmqID length], NULL) != SQLITE_OK) {
+ _FIRMessagingRmqLogAndExit(stmt, NO);
+ }
+
+ if (sqlite3_step(stmt) != SQLITE_DONE) {
+ _FIRMessagingRmqLogAndExit(stmt, NO);
+ }
+
+ sqlite3_finalize(stmt);
+ return YES;
+
+}
+
+#pragma mark - Private
+
+- (NSString *)lastErrorMessage {
+ return [NSString stringWithFormat:@"%s", sqlite3_errmsg(_database)];
+}
+
+- (int)lastErrorCode {
+ return sqlite3_errcode(_database);
+}
+
+- (void)logError {
+ FIRMessagingLoggerError(kFIRMessagingMessageCodeRmq2PersistentStore006,
+ @"%@ error: code (%d) message: %@", kFCMRmqStoreTag, [self lastErrorCode],
+ [self lastErrorMessage]);
+}
+
+- (void)logErrorAndFinalizeStatement:(sqlite3_stmt *)stmt {
+ [self logError];
+ sqlite3_finalize(stmt);
+}
+
+@end
diff --git a/Firebase/Messaging/FIRMessagingRmqManager.h b/Firebase/Messaging/FIRMessagingRmqManager.h
new file mode 100644
index 0000000..ba48b98
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingRmqManager.h
@@ -0,0 +1,190 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+@class GtalkDataMessageStanza;
+@class GPBMessage;
+
+@class FIRMessagingPersistentSyncMessage;
+
+/**
+ * Called on each raw message.
+ */
+typedef void(^FIRMessagingRmqMessageHandler)(int64_t rmqId, int8_t tag, NSData *data);
+
+/**
+ * Called on each DataMessageStanza.
+ */
+typedef void(^FIRMessagingDataMessageHandler)(int64_t rmqId, GtalkDataMessageStanza *stanza);
+
+/**
+ * Used to scan through the rmq and perform actions on messages as required.
+ */
+@protocol FIRMessagingRmqScanner <NSObject>
+
+/**
+ * Scan the RMQ for outgoing messages and process them as required.
+ */
+- (void)scanWithRmqMessageHandler:(FIRMessagingRmqMessageHandler)rmqMessageHandler
+ dataMessageHandler:(FIRMessagingDataMessageHandler)dataMessageHandler;
+
+@end
+
+/**
+ * This manages the RMQ persistent store.
+ *
+ * The store is used to store all the S2D id's that were received by the client and were ACK'ed
+ * by us but the server hasn't confirmed the ACK. We don't delete these id's until the server
+ * ACK's us that they have received them.
+ *
+ * We also store the upstream messages(d2s) that were sent by the client.
+ *
+ * Also store the lastRMQId that was sent by us so that for a new connection being setup we don't
+ * duplicate RMQ Id's for the new messages.
+ */
+@interface FIRMessagingRmqManager : NSObject <FIRMessagingRmqScanner>
+
+// designated initializer
+- (instancetype)initWithDatabaseName:(NSString *)databaseName;
+
+- (void)loadRmqId;
+
+/**
+ * Save an upstream message to RMQ. If the message send fails for some reason we would not
+ * lose the message since it would be saved in the RMQ.
+ *
+ * @param message The upstream message to be saved.
+ * @param error The error if any while saving the message else nil.
+ *
+ * @return YES if the message was successfully saved to RMQ else NO.
+ */
+- (BOOL)saveRmqMessage:(GPBMessage *)message error:(NSError **)error;
+
+/**
+ * Save Server to device message with the given RMQ-ID.
+ *
+ * @param rmqID The rmqID of the s2d message to save.
+ *
+ * @return YES if the save was successfull else NO.
+ */
+- (BOOL)saveS2dMessageWithRmqId:(NSString *)rmqID;
+
+/**
+ * A list of all unacked Server to device RMQ IDs.
+ *
+ * @return A list of unacked Server to Device RMQ ID's. All values are Strings.
+ */
+- (NSArray *)unackedS2dRmqIds;
+
+/**
+ * Removes the outgoing message from RMQ store.
+ *
+ * @param rmqId The rmqID to remove from the store.
+ *
+ * @return The number of messages deleted successfully.
+ */
+- (int)removeRmqMessagesWithRmqId:(NSString *)rmqId;
+
+/**
+ * Removes the messages with the given rmqIDs from RMQ store.
+ *
+ * @param rmqIds The lsit of rmqID's to remove from the store.
+ *
+ * @return The number of messages deleted successfully.
+ */
+- (int)removeRmqMessagesWithRmqIds:(NSArray *)rmqIds;
+
+/**
+ * Removes a list of downstream messages from the RMQ.
+ *
+ * @param s2dIds The list of messages ACK'ed by the server that we should remove
+ * from the RMQ store.
+ */
+- (void)removeS2dIds:(NSArray *)s2dIds;
+
+#pragma mark - Sync Messages
+
+/**
+ * Get persisted sync message with rmqID.
+ *
+ * @param rmqID The rmqID of the persisted sync message.
+ *
+ * @return A valid persistent sync message with the given rmqID if found in the RMQ else nil.
+ */
+- (FIRMessagingPersistentSyncMessage *)querySyncMessageWithRmqID:(NSString *)rmqID;
+
+/**
+ * Delete sync message with rmqID.
+ *
+ * @param rmqID The rmqID of the persisted sync message.
+ *
+ * @return YES if the message was successfully deleted else NO.
+ */
+- (BOOL)deleteSyncMessageWithRmqID:(NSString *)rmqID;
+
+/**
+ * Delete the expired sync messages from persisten store. Also deletes messages that have been
+ * delivered both via APNS and MCS.
+ *
+ * @param error The error if any while deleting the messages.
+ *
+ * @return The total number of messages that were deleted from the persistent store.
+ */
+- (int)deleteExpiredOrFinishedSyncMessages:(NSError **)error;
+
+/**
+ * Save sync message received by the device.
+ *
+ * @param rmqID The rmqID of the message received.
+ * @param expirationTime The expiration time of the sync message received.
+ * @param apnsReceived YES if the message was received via APNS else NO.
+ * @param mcsReceived YES if the message was received via MCS else NO.
+ * @param error The error if any while saving the sync message to persistent store.
+ *
+ * @return YES if the message save was successful else NO.
+ */
+- (BOOL)saveSyncMessageWithRmqID:(NSString *)rmqID
+ expirationTime:(int64_t)expirationTime
+ apnsReceived:(BOOL)apnsReceived
+ mcsReceived:(BOOL)mcsReceived
+ error:(NSError **)error;
+
+/**
+ * Update sync message received via APNS.
+ *
+ * @param rmqID The rmqID of the received message.
+ * @param error The error if any while updating the sync message.
+ *
+ * @return YES if the persistent sync message was successfully updated else NO.
+ */
+- (BOOL)updateSyncMessageViaAPNSWithRmqID:(NSString *)rmqID error:(NSError **)error;
+
+/**
+ * Update sync message received via MCS.
+ *
+ * @param rmqID The rmqID of the received message.
+ * @param error The error if any while updating the sync message.
+ *
+ * @return YES if the persistent sync message was successfully updated else NO.
+ */
+- (BOOL)updateSyncMessageViaMCSWithRmqID:(NSString *)rmqID error:(NSError **)error;
+
+#pragma mark - Testing
+
++ (void)removeDatabaseWithName:(NSString *)dbName;
+
+@end
diff --git a/Firebase/Messaging/FIRMessagingRmqManager.m b/Firebase/Messaging/FIRMessagingRmqManager.m
new file mode 100644
index 0000000..de63a73
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingRmqManager.m
@@ -0,0 +1,264 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRMessagingRmqManager.h"
+
+#import "Protos/GtalkCore.pbobjc.h"
+#import "sqlite3.h"
+
+#import "FIRMessagingDefines.h"
+#import "FIRMessagingLogger.h"
+#import "FIRMessagingRmq2PersistentStore.h"
+#import "FIRMessagingUtilities.h"
+
+#ifndef _FIRMessagingRmqLogAndExit
+#define _FIRMessagingRmqLogAndExit(stmt, return_value) \
+do { \
+ [self logErrorAndFinalizeStatement:stmt]; \
+ return return_value; \
+} while(0)
+#endif
+
+static NSString *const kFCMRmqTag = @"FIRMessagingRmq:";
+
+@interface FIRMessagingRmqManager ()
+
+@property(nonatomic, readwrite, strong) FIRMessagingRmq2PersistentStore *rmq2Store;
+// map the category of an outgoing message with the number of messages for that category
+// should always have two keys -- the app, gcm
+@property(nonatomic, readwrite, strong) NSMutableDictionary *outstandingMessages;
+
+// Outgoing RMQ persistent id
+@property(nonatomic, readwrite, assign) int64_t rmqId;
+
+@end
+
+@implementation FIRMessagingRmqManager
+
+- (instancetype)initWithDatabaseName:(NSString *)databaseName {
+ self = [super init];
+ if (self) {
+ _FIRMessagingDevAssert([databaseName length] > 0, @"RMQ: Invalid rmq db name");
+ _rmq2Store = [[FIRMessagingRmq2PersistentStore alloc] initWithDatabaseName:databaseName];
+ _outstandingMessages = [NSMutableDictionary dictionaryWithCapacity:2];
+ _rmqId = -1;
+ }
+ return self;
+}
+
+- (void)loadRmqId {
+ if (self.rmqId >= 0) {
+ return; // already done
+ }
+
+ [self loadInitialOutgoingPersistentId];
+ if (self.outstandingMessages.count) {
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeRmqManager000,
+ @"%@: outstanding categories %ld", kFCMRmqTag,
+ _FIRMessaging_UL(self.outstandingMessages.count));
+ }
+}
+
+/**
+ * Initialize the 'initial RMQ':
+ * - max ID of any message in the queue
+ * - if the queue is empty, stored value in separate DB.
+ *
+ * Stream acks will remove from RMQ, when we remove the highest message we keep track
+ * of its ID.
+ */
+- (void)loadInitialOutgoingPersistentId {
+
+ // we shouldn't always trust the lastRmqId stored in the LastRmqId table, because
+ // we only save to the LastRmqId table once in a while (after getting the lastRmqId sent
+ // by the server after reconnect, and after getting a rmq ack from the server). The
+ // rmq message with the highest rmq id tells the real story, so check against that first.
+
+ int64_t rmqId = [self queryHighestRmqId];
+ if (rmqId == 0) {
+ rmqId = [self querylastRmqId];
+ }
+ self.rmqId = rmqId + 1;
+}
+
+#pragma mark - Save
+
+/**
+ * Save a message to RMQ2. Will populate the rmq2 persistent ID.
+ */
+- (BOOL)saveRmqMessage:(GPBMessage *)message
+ error:(NSError **)error {
+ // send using rmq2manager
+ // the wire format of rmq2 id is a string. However, we keep it as a long internally
+ // in the database. So only convert the id to string when preparing for sending over
+ // the wire.
+ NSString *rmq2Id = FIRMessagingGetRmq2Id(message);
+ if (![rmq2Id length]) {
+ int64_t rmqId = [self nextRmqId];
+ rmq2Id = [NSString stringWithFormat:@"%lld", rmqId];
+ FIRMessagingSetRmq2Id(message, rmq2Id);
+ }
+ FIRMessagingProtoTag tag = FIRMessagingGetTagForProto(message);
+ return [self saveMessage:message withRmqId:[rmq2Id integerValue] tag:tag error:error];
+}
+
+- (BOOL)saveMessage:(GPBMessage *)message
+ withRmqId:(int64_t)rmqId
+ tag:(int8_t)tag
+ error:(NSError **)error {
+ NSData *data = [message data];
+ return [self.rmq2Store saveMessageWithRmqId:rmqId tag:tag data:data error:error];
+}
+
+/**
+ * This is called when we delete the largest outgoing message from queue.
+ */
+- (void)saveLastOutgoingRmqId:(int64_t)rmqID {
+ [self.rmq2Store updateLastOutgoingRmqId:rmqID];
+}
+
+- (BOOL)saveS2dMessageWithRmqId:(NSString *)rmqID {
+ return [self.rmq2Store saveUnackedS2dMessageWithRmqId:rmqID];
+}
+
+#pragma mark - Query
+
+- (int64_t)queryHighestRmqId {
+ return [self.rmq2Store queryHighestRmqId];
+}
+
+- (int64_t)querylastRmqId {
+ return [self.rmq2Store queryLastRmqId];
+}
+
+- (NSArray *)unackedS2dRmqIds {
+ return [self.rmq2Store unackedS2dRmqIds];
+}
+
+#pragma mark - FIRMessagingRMQScanner protocol
+
+/**
+ * We don't have a 'getMessages' method - it would require loading in memory
+ * the entire content body of all messages.
+ *
+ * Instead we iterate and call 'resend' for each message.
+ *
+ * This is called:
+ * - on connect MCS, to resend any outstanding messages
+ * - init
+ */
+- (void)scanWithRmqMessageHandler:(FIRMessagingRmqMessageHandler)rmqMessageHandler
+ dataMessageHandler:(FIRMessagingDataMessageHandler)dataMessageHandler {
+ // no need to scan database with no callbacks
+ if (rmqMessageHandler || dataMessageHandler) {
+ [self.rmq2Store scanOutgoingRmqMessagesWithHandler:^(int64_t rmqId, int8_t tag, NSData *data) {
+ if (rmqMessageHandler != nil) {
+ rmqMessageHandler(rmqId, tag, data);
+ }
+ if (dataMessageHandler != nil && kFIRMessagingProtoTagDataMessageStanza == tag) {
+ GPBMessage *proto =
+ [FIRMessagingGetClassForTag((FIRMessagingProtoTag)tag) parseFromData:data error:NULL];
+ GtalkDataMessageStanza *stanza = (GtalkDataMessageStanza *)proto;
+ dataMessageHandler(rmqId, stanza);
+ }
+ }];
+ }
+}
+
+#pragma mark - Remove
+
+- (void)ackReceivedForRmqId:(NSString *)rmqId {
+ // TODO: Optional book-keeping
+}
+
+- (int)removeRmqMessagesWithRmqId:(NSString *)rmqId {
+ return [self removeRmqMessagesWithRmqIds:@[rmqId]];
+}
+
+- (int)removeRmqMessagesWithRmqIds:(NSArray *)rmqIds {
+ if (![rmqIds count]) {
+ return 0;
+ }
+ for (NSString *rmqId in rmqIds) {
+ [self ackReceivedForRmqId:rmqId];
+ }
+ int64_t maxRmqId = -1;
+ for (NSString *rmqId in rmqIds) {
+ int64_t rmqIdValue = [rmqId longLongValue];
+ if (rmqIdValue > maxRmqId) {
+ maxRmqId = rmqIdValue;
+ }
+ }
+ maxRmqId++;
+ if (maxRmqId >= self.rmqId) {
+ [self saveLastOutgoingRmqId:maxRmqId];
+ }
+ return [self.rmq2Store deleteMessagesFromTable:kTableOutgoingRmqMessages withRmqIds:rmqIds];
+}
+
+- (void)removeS2dIds:(NSArray *)s2dIds {
+ [self.rmq2Store deleteMessagesFromTable:kTableS2DRmqIds withRmqIds:s2dIds];
+}
+
+#pragma mark - Sync Messages
+
+// TODO: RMQManager should also have a cache for all the sync messages
+// so we don't hit the DB each time.
+- (FIRMessagingPersistentSyncMessage *)querySyncMessageWithRmqID:(NSString *)rmqID {
+ return [self.rmq2Store querySyncMessageWithRmqID:rmqID];
+}
+
+- (BOOL)deleteSyncMessageWithRmqID:(NSString *)rmqID {
+ return [self.rmq2Store deleteSyncMessageWithRmqID:rmqID];
+}
+
+- (int)deleteExpiredOrFinishedSyncMessages:(NSError **)error {
+ return [self.rmq2Store deleteExpiredOrFinishedSyncMessages:error];
+}
+
+- (BOOL)saveSyncMessageWithRmqID:(NSString *)rmqID
+ expirationTime:(int64_t)expirationTime
+ apnsReceived:(BOOL)apnsReceived
+ mcsReceived:(BOOL)mcsReceived
+ error:(NSError *__autoreleasing *)error {
+ return [self.rmq2Store saveSyncMessageWithRmqID:rmqID
+ expirationTime:expirationTime
+ apnsReceived:apnsReceived
+ mcsReceived:mcsReceived
+ error:error];
+}
+
+- (BOOL)updateSyncMessageViaAPNSWithRmqID:(NSString *)rmqID error:(NSError **)error {
+ return [self.rmq2Store updateSyncMessageViaAPNSWithRmqID:rmqID error:error];
+}
+
+- (BOOL)updateSyncMessageViaMCSWithRmqID:(NSString *)rmqID error:(NSError **)error {
+ return [self.rmq2Store updateSyncMessageViaMCSWithRmqID:rmqID error:error];
+}
+
+#pragma mark - Testing
+
++ (void)removeDatabaseWithName:(NSString *)dbName {
+ [FIRMessagingRmq2PersistentStore removeDatabase:dbName];
+}
+
+#pragma mark - Private
+
+- (int64_t)nextRmqId {
+ return ++self.rmqId;
+}
+
+@end
diff --git a/Firebase/Messaging/FIRMessagingSecureSocket.h b/Firebase/Messaging/FIRMessagingSecureSocket.h
new file mode 100644
index 0000000..169f60e
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingSecureSocket.h
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+typedef NS_ENUM(NSUInteger, FIRMessagingSecureSocketState){
+ kFIRMessagingSecureSocketNotOpen = 0,
+ kFIRMessagingSecureSocketOpening,
+ kFIRMessagingSecureSocketOpen,
+ kFIRMessagingSecureSocketClosing,
+ kFIRMessagingSecureSocketClosed,
+ kFIRMessagingSecureSocketError
+};
+
+@class FIRMessagingSecureSocket;
+
+@protocol FIRMessagingSecureSocketDelegate<NSObject>
+
+- (void)secureSocket:(FIRMessagingSecureSocket *)socket
+ didReceiveData:(NSData *)data
+ withTag:(int8_t)tag;
+- (void)secureSocket:(FIRMessagingSecureSocket *)socket
+ didSendProtoWithTag:(int8_t)tag
+ rmqId:(NSString *)rmqId;
+- (void)secureSocketDidConnect:(FIRMessagingSecureSocket *)socket;
+- (void)didDisconnectWithSecureSocket:(FIRMessagingSecureSocket *)socket;
+
+@end
+
+/**
+ * This manages the input/output streams connected to the MCS server. Used to receive data from
+ * the server and send to it over the wire.
+ */
+@interface FIRMessagingSecureSocket : NSObject
+
+@property(nonatomic, readwrite, weak) id<FIRMessagingSecureSocketDelegate> delegate;
+@property(nonatomic, readonly, assign) FIRMessagingSecureSocketState state;
+
+- (void)connectToHost:(NSString *)host port:(NSUInteger)port onRunLoop:(NSRunLoop *)runLoop;
+- (void)disconnect;
+- (void)sendData:(NSData *)data withTag:(int8_t)tag rmqId:(NSString *)rmqId;
+
+@end
diff --git a/Firebase/Messaging/FIRMessagingSecureSocket.m b/Firebase/Messaging/FIRMessagingSecureSocket.m
new file mode 100644
index 0000000..b7e8133
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingSecureSocket.m
@@ -0,0 +1,448 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRMessagingSecureSocket.h"
+
+#import "GPBMessage.h"
+#import "GPBCodedOutputStream.h"
+#import "GPBUtilities.h"
+
+#import "FIRMessagingCodedInputStream.h"
+#import "FIRMessagingDefines.h"
+#import "FIRMessagingLogger.h"
+#import "FIRMessagingPacketQueue.h"
+
+static const NSUInteger kMaxBufferLength = 1024 * 1024; // 1M
+static const NSUInteger kBufferLengthIncrement = 16 * 1024; // 16k
+static const uint8_t kVersion = 40;
+static const uint8_t kInvalidTag = -1;
+
+typedef NS_ENUM(NSUInteger, FIRMessagingSecureSocketReadResult) {
+ kFIRMessagingSecureSocketReadResultNone,
+ kFIRMessagingSecureSocketReadResultIncomplete,
+ kFIRMessagingSecureSocketReadResultCorrupt,
+ kFIRMessagingSecureSocketReadResultSuccess
+};
+
+static int32_t LogicalRightShift32(int32_t value, int32_t spaces) {
+ return (int32_t)((uint32_t)(value) >> spaces);
+}
+
+static NSUInteger SerializedSize(int32_t value) {
+ NSUInteger bytes = 0;
+ while (YES) {
+ if ((value & ~0x7F) == 0) {
+ bytes += sizeof(uint8_t);
+ return bytes;
+ } else {
+ bytes += sizeof(uint8_t);
+ value = LogicalRightShift32(value, 7);
+ }
+ }
+}
+
+@interface FIRMessagingSecureSocket() <NSStreamDelegate>
+
+@property(nonatomic, readwrite, assign) FIRMessagingSecureSocketState state;
+@property(nonatomic, readwrite, strong) NSInputStream *inStream;
+@property(nonatomic, readwrite, strong) NSOutputStream *outStream;
+
+@property(nonatomic, readwrite, strong) NSMutableData *inputBuffer;
+@property(nonatomic, readwrite, assign) NSUInteger inputBufferLength;
+@property(nonatomic, readwrite, strong) NSMutableData *outputBuffer;
+@property(nonatomic, readwrite, assign) NSUInteger outputBufferLength;
+
+@property(nonatomic, readwrite, strong) FIRMessagingPacketQueue *packetQueue;
+@property(nonatomic, readwrite, assign) BOOL isVersionSent;
+@property(nonatomic, readwrite, assign) BOOL isVersionReceived;
+@property(nonatomic, readwrite, assign) BOOL isInStreamOpen;
+@property(nonatomic, readwrite, assign) BOOL isOutStreamOpen;
+
+@property(nonatomic, readwrite, strong) NSRunLoop *runLoop;
+@property(nonatomic, readwrite, strong) NSString *currentRmqIdBeingSent;
+@property(nonatomic, readwrite, assign) int8_t currentProtoTypeBeingSent;
+
+@end
+
+@implementation FIRMessagingSecureSocket
+
+- (instancetype)init {
+ self = [super init];
+ if (self) {
+ _state = kFIRMessagingSecureSocketNotOpen;
+ _inputBuffer = [NSMutableData dataWithLength:kBufferLengthIncrement];
+ _packetQueue = [[FIRMessagingPacketQueue alloc] init];
+ _currentProtoTypeBeingSent = kInvalidTag;
+ }
+ return self;
+}
+
+- (void)dealloc {
+ [self disconnect];
+}
+
+- (void)connectToHost:(NSString *)host
+ port:(NSUInteger)port
+ onRunLoop:(NSRunLoop *)runLoop {
+ _FIRMessagingDevAssert(host != nil, @"Invalid host");
+ _FIRMessagingDevAssert(runLoop != nil, @"Invalid runloop");
+ _FIRMessagingDevAssert(self.state == kFIRMessagingSecureSocketNotOpen, @"Socket is already connected");
+
+ if (!host || self.state != kFIRMessagingSecureSocketNotOpen) {
+ return;
+ }
+
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeSecureSocket000,
+ @"Opening secure socket to FIRMessaging service");
+ self.state = kFIRMessagingSecureSocketOpening;
+ self.runLoop = runLoop;
+ CFReadStreamRef inputStreamRef;
+ CFWriteStreamRef outputStreamRef;
+ CFStreamCreatePairWithSocketToHost(NULL,
+ (__bridge CFStringRef)host,
+ (int)port,
+ &inputStreamRef,
+ &outputStreamRef);
+ self.inStream = CFBridgingRelease(inputStreamRef);
+ self.outStream = CFBridgingRelease(outputStreamRef);
+ if (!self.inStream || !self.outStream) {
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeSecureSocket001,
+ @"Failed to initialize socket.");
+ return;
+ }
+
+ self.isInStreamOpen = NO;
+ self.isOutStreamOpen = NO;
+
+ BOOL isVOIPSocket = NO;
+
+#if FIRMessaging_PROBER
+ isVOIPSocket = YES;
+#endif
+
+ [self openStream:self.outStream isVOIPStream:isVOIPSocket];
+ [self openStream:self.inStream isVOIPStream:isVOIPSocket];
+}
+
+- (void)disconnect {
+ if (self.state == kFIRMessagingSecureSocketClosing) {
+ return;
+ }
+ if (!self.inStream && !self.outStream) {
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeSecureSocket002,
+ @"The socket is not open or already closed.");
+ _FIRMessagingDevAssert(self.state == kFIRMessagingSecureSocketClosed || self.state == kFIRMessagingSecureSocketNotOpen,
+ @"Socket is already disconnected.");
+ return;
+ }
+
+ self.state = kFIRMessagingSecureSocketClosing;
+ if (self.inStream) {
+ [self closeStream:self.inStream];
+ self.inStream = nil;
+ }
+ if (self.outStream) {
+ [self closeStream:self.outStream];
+ self.outStream = nil;
+ }
+ self.state = kFIRMessagingSecureSocketClosed;
+ [self.delegate didDisconnectWithSecureSocket:self];
+}
+
+- (void)sendData:(NSData *)data withTag:(int8_t)tag rmqId:(NSString *)rmqId {
+ [self.packetQueue push:[FIRMessagingPacket packetWithTag:tag rmqId:rmqId data:data]];
+ if ([self.outStream hasSpaceAvailable]) {
+ [self performWrite];
+ }
+}
+
+#pragma mark - NSStreamDelegate
+
+- (void)stream:(NSStream *)stream handleEvent:(NSStreamEvent)eventCode {
+ switch (eventCode) {
+ case NSStreamEventHasBytesAvailable:
+ if (self.state != kFIRMessagingSecureSocketOpen) {
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeSecureSocket003,
+ @"Try to read from socket that is not opened");
+ return;
+ }
+ _FIRMessagingDevAssert(stream == self.inStream, @"Incorrect stream");
+ if (![self performRead]) {
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeSecureSocket004,
+ @"Error occured when reading incoming stream");
+ [self disconnect];
+ }
+ break;
+ case NSStreamEventEndEncountered:
+ FIRMessagingLoggerDebug(
+ kFIRMessagingMessageCodeSecureSocket005, @"%@ end encountered",
+ stream == self.inStream
+ ? @"Input stream"
+ : (stream == self.outStream ? @"Output stream" : @"Unknown stream"));
+ [self disconnect];
+ break;
+ case NSStreamEventOpenCompleted:
+ if (stream == self.inStream) {
+ self.isInStreamOpen = YES;
+ } else if (stream == self.outStream) {
+ self.isOutStreamOpen = YES;
+ }
+ if (self.isInStreamOpen && self.isOutStreamOpen) {
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeSecureSocket006,
+ @"Secure socket to FIRMessaging service opened");
+ self.state = kFIRMessagingSecureSocketOpen;
+ [self.delegate secureSocketDidConnect:self];
+ }
+ break;
+ case NSStreamEventErrorOccurred: {
+ FIRMessagingLoggerDebug(
+ kFIRMessagingMessageCodeSecureSocket007, @"%@ error occurred",
+ stream == self.inStream
+ ? @"Input stream"
+ : (stream == self.outStream ? @"Output stream" : @"Unknown stream"));
+ [self disconnect];
+ break;
+ }
+ case NSStreamEventHasSpaceAvailable:
+ if (self.state != kFIRMessagingSecureSocketOpen) {
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeSecureSocket008,
+ @"Try to write to socket that is not opened");
+ return;
+ }
+ _FIRMessagingDevAssert(stream == self.outStream, @"Incorrect stream");
+ [self performWrite];
+ break;
+ default:
+ break;
+ }
+}
+
+#pragma mark - Private
+
+- (void)openStream:(NSStream *)stream isVOIPStream:(BOOL)isVOIPStream {
+ _FIRMessagingDevAssert(stream != nil, @"Invalid stream");
+ _FIRMessagingDevAssert(self.runLoop != nil, @"Invalid runloop");
+
+ if (stream) {
+ _FIRMessagingDevAssert([stream streamStatus] == NSStreamStatusNotOpen, @"Stream already open");
+ if ([stream streamStatus] != NSStreamStatusNotOpen) {
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeSecureSocket009,
+ @"stream should not be open.");
+ return;
+ }
+ [stream setProperty:NSStreamSocketSecurityLevelNegotiatedSSL
+ forKey:NSStreamSocketSecurityLevelKey];
+ if (isVOIPStream) {
+ [stream setProperty:NSStreamNetworkServiceTypeVoIP
+ forKey:NSStreamNetworkServiceType];
+ }
+ stream.delegate = self;
+ [stream scheduleInRunLoop:self.runLoop forMode:NSDefaultRunLoopMode];
+ [stream open];
+ }
+}
+
+- (void)closeStream:(NSStream *)stream {
+ _FIRMessagingDevAssert(stream != nil, @"Invalid stream");
+ _FIRMessagingDevAssert(self.runLoop != nil, @"Invalid runloop");
+
+ if (stream) {
+ [stream close];
+ [stream removeFromRunLoop:self.runLoop forMode:NSDefaultRunLoopMode];
+ stream.delegate = nil;
+ }
+}
+
+- (BOOL)performRead {
+ _FIRMessagingDevAssert(self.state == kFIRMessagingSecureSocketOpen, @"Socket should be open");
+
+ if (!self.isVersionReceived) {
+ self.isVersionReceived = YES;
+ uint8_t versionByte = 0;
+ NSInteger bytesRead = [self.inStream read:&versionByte maxLength:sizeof(uint8_t)];
+ if (bytesRead != sizeof(uint8_t) || kVersion != versionByte) {
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeSecureSocket010,
+ @"Version do not match. Received %d, Expecting %d", versionByte,
+ kVersion);
+ return NO;
+ }
+ }
+
+ while (YES) {
+ BOOL isInputBufferValid = [self.inputBuffer length] > 0;
+ _FIRMessagingDevAssert(isInputBufferValid,
+ @"Invalid input buffer size %lu. Used bytes length %lu, buffer content: %@",
+ _FIRMessaging_UL([self.inputBuffer length]),
+ _FIRMessaging_UL(self.inputBufferLength),
+ self.inputBuffer);
+ if (!isInputBufferValid) {
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeSecureSocket011,
+ @"Input buffer is not valid.");
+ return NO;
+ }
+
+ if (![self.inStream hasBytesAvailable]) {
+ break;
+ }
+
+ // try to read more data
+ uint8_t *unusedBufferPtr = (uint8_t *)self.inputBuffer.mutableBytes + self.inputBufferLength;
+ NSUInteger unusedBufferLength = [self.inputBuffer length] - self.inputBufferLength;
+ NSInteger bytesRead = [self.inStream read:unusedBufferPtr maxLength:unusedBufferLength];
+ if (bytesRead <= 0) {
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeSecureSocket012,
+ @"Failed to read input stream. Bytes read %ld, Used buffer size %lu, "
+ @"Unused buffer size %lu",
+ _FIRMessaging_UL(bytesRead), _FIRMessaging_UL(self.inputBufferLength),
+ _FIRMessaging_UL(unusedBufferLength));
+ break;
+ }
+ // did successfully read some more data
+ self.inputBufferLength += (NSUInteger)bytesRead;
+
+ if ([self.inputBuffer length] <= self.inputBufferLength) {
+ // shouldn't be reading more than 1MB of data in one go
+ if ([self.inputBuffer length] + kBufferLengthIncrement > kMaxBufferLength) {
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeSecureSocket013,
+ @"Input buffer exceed 1M, disconnect socket");
+ return NO;
+ }
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeSecureSocket014,
+ @"Input buffer limit exceeded. Used input buffer size %lu, "
+ @"Total input buffer size %lu. No unused buffer left. "
+ @"Increase buffer size.",
+ _FIRMessaging_UL(self.inputBufferLength),
+ _FIRMessaging_UL([self.inputBuffer length]));
+ [self.inputBuffer increaseLengthBy:kBufferLengthIncrement];
+ _FIRMessagingDevAssert([self.inputBuffer length] > self.inputBufferLength, @"Invalid buffer size");
+ }
+
+ while (self.inputBufferLength > 0 && [self.inputBuffer length] > 0) {
+ _FIRMessagingDevAssert([self.inputBuffer length] >= self.inputBufferLength,
+ @"Buffer longer than length");
+ NSRange inputRange = NSMakeRange(0, self.inputBufferLength);
+ size_t protoBytes = 0;
+ // read the actual proto data coming in
+ FIRMessagingSecureSocketReadResult readResult =
+ [self processCurrentInputBuffer:[self.inputBuffer subdataWithRange:inputRange]
+ outOffset:&protoBytes];
+ // Corrupt data encountered, stop processing.
+ if (readResult == kFIRMessagingSecureSocketReadResultCorrupt) {
+ return NO;
+ // Incomplete data, keep trying to read by loading more from the stream.
+ } else if (readResult == kFIRMessagingSecureSocketReadResultIncomplete) {
+ break;
+ }
+ _FIRMessagingDevAssert(self.inputBufferLength >= protoBytes, @"More bytes than buffer can handle");
+ // we have read (0, protoBytes) of data in the inputBuffer
+ if (protoBytes == self.inputBufferLength) {
+ // did completely read the buffer data can be reset for further processing
+ self.inputBufferLength = 0;
+ } else {
+ // delete processed bytes while maintaining the buffer size.
+ NSUInteger prevLength __unused = [self.inputBuffer length];
+ // delete the processed bytes
+ [self.inputBuffer replaceBytesInRange:NSMakeRange(0, protoBytes) withBytes:NULL length:0];
+ // reallocate more data
+ [self.inputBuffer increaseLengthBy:protoBytes];
+ _FIRMessagingDevAssert([self.inputBuffer length] == prevLength,
+ @"Invalid input buffer size %lu. Used bytes length %lu, "
+ @"buffer content: %@",
+ _FIRMessaging_UL([self.inputBuffer length]),
+ _FIRMessaging_UL(self.inputBufferLength),
+ self.inputBuffer);
+ self.inputBufferLength -= protoBytes;
+ }
+ }
+ }
+ return YES;
+}
+
+- (FIRMessagingSecureSocketReadResult)processCurrentInputBuffer:(NSData *)readData
+ outOffset:(size_t *)outOffset {
+ *outOffset = 0;
+
+ FIRMessagingCodedInputStream *input = [[FIRMessagingCodedInputStream alloc] initWithData:readData];
+ int8_t rawTag;
+ if (![input readTag:&rawTag]) {
+ return kFIRMessagingSecureSocketReadResultIncomplete;
+ }
+ int32_t length;
+ if (![input readLength:&length]) {
+ return kFIRMessagingSecureSocketReadResultIncomplete;
+ }
+ // NOTE tag can be zero for |HeartbeatPing|, and length can be zero for |Close| proto
+ _FIRMessagingDevAssert(rawTag >= 0 && length >= 0, @"Invalid tag or length");
+ if (rawTag < 0 || length < 0) {
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeSecureSocket015, @"Buffer data corrupted.");
+ return kFIRMessagingSecureSocketReadResultCorrupt;
+ }
+ NSData *data = [input readDataWithLength:(uint32_t)length];
+ if (data == nil) {
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeSecureSocket016,
+ @"Incomplete data, buffered data length %ld, expected length %d",
+ _FIRMessaging_UL(self.inputBufferLength), length);
+ return kFIRMessagingSecureSocketReadResultIncomplete;
+ }
+ [self.delegate secureSocket:self didReceiveData:data withTag:rawTag];
+ *outOffset = input.offset;
+ return kFIRMessagingSecureSocketReadResultSuccess;
+}
+
+- (void)performWrite {
+ _FIRMessagingDevAssert(self.state == kFIRMessagingSecureSocketOpen, @"Invalid socket state");
+
+ if (!self.isVersionSent) {
+ self.isVersionSent = YES;
+ uint8_t versionByte = kVersion;
+ [self.outStream write:&versionByte maxLength:sizeof(uint8_t)];
+ }
+
+ while (!self.packetQueue.isEmpty && self.outStream.hasSpaceAvailable) {
+ if (self.outputBuffer.length == 0) {
+ // serialize new packets only when the output buffer is flushed.
+ FIRMessagingPacket *packet = [self.packetQueue pop];
+ self.currentRmqIdBeingSent = packet.rmqId;
+ self.currentProtoTypeBeingSent = packet.tag;
+ NSUInteger length = SerializedSize(packet.tag) +
+ SerializedSize((int)packet.data.length) + packet.data.length;
+ self.outputBuffer = [NSMutableData dataWithLength:length];
+ GPBCodedOutputStream *output = [GPBCodedOutputStream streamWithData:self.outputBuffer];
+ [output writeRawVarint32:packet.tag];
+ [output writeBytesNoTag:packet.data];
+ self.outputBufferLength = 0;
+ }
+
+ // flush the output buffer.
+ NSInteger written = [self.outStream write:self.outputBuffer.bytes + self.outputBufferLength
+ maxLength:self.outputBuffer.length - self.outputBufferLength];
+ if (written <= 0) {
+ continue;
+ }
+ self.outputBufferLength += (NSUInteger)written;
+ if (self.outputBufferLength >= self.outputBuffer.length) {
+ self.outputBufferLength = 0;
+ self.outputBuffer = nil;
+ [self.delegate secureSocket:self
+ didSendProtoWithTag:self.currentProtoTypeBeingSent
+ rmqId:self.currentRmqIdBeingSent];
+ self.currentRmqIdBeingSent = nil;
+ self.currentProtoTypeBeingSent = kInvalidTag;
+ }
+ }
+}
+
+@end
diff --git a/Firebase/Messaging/FIRMessagingSyncMessageManager.h b/Firebase/Messaging/FIRMessagingSyncMessageManager.h
new file mode 100644
index 0000000..3d30bdb
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingSyncMessageManager.h
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+@class FIRMessagingRmqManager;
+
+/**
+ * Handle sync messages being received both via MCS and APNS.
+ */
+@interface FIRMessagingSyncMessageManager : NSObject
+
+/**
+ * Initialize sync message manager.
+ *
+ * @param rmqManager The RMQ manager on the client.
+ *
+ * @return Sync message manager.
+ */
+- (instancetype)initWithRmqManager:(FIRMessagingRmqManager *)rmqManager;
+
+/**
+ * Remove expired sync message from persistent store. Also removes messages that have
+ * been received both via APNS and MCS.
+ */
+- (void)removeExpiredSyncMessages;
+
+/**
+ * App did recive a sync message via APNS.
+ *
+ * @param message The sync message received.
+ *
+ * @return YES if the message is a duplicate of an already received sync message else NO.
+ */
+- (BOOL)didReceiveAPNSSyncMessage:(NSDictionary *)message;
+
+/**
+ * App did receive a sync message via MCS.
+ *
+ * @param message The sync message received.
+ *
+ * @return YES if the message is a duplicate of an already received sync message else NO.
+ */
+- (BOOL)didReceiveMCSSyncMessage:(NSDictionary *)message;
+
+@end
diff --git a/Firebase/Messaging/FIRMessagingSyncMessageManager.m b/Firebase/Messaging/FIRMessagingSyncMessageManager.m
new file mode 100644
index 0000000..1257b02
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingSyncMessageManager.m
@@ -0,0 +1,147 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRMessagingSyncMessageManager.h"
+
+#import "FIRMessagingConstants.h"
+#import "FIRMessagingDefines.h"
+#import "FIRMessagingLogger.h"
+#import "FIRMessagingPersistentSyncMessage.h"
+#import "FIRMessagingRmqManager.h"
+#import "FIRMessagingUtilities.h"
+
+static const int64_t kDefaultSyncMessageTTL = 4 * 7 * 24 * 60 * 60; // 4 weeks
+// 4 MB of free space is required to persist Sync messages
+static const uint64_t kMinFreeDiskSpaceInMB = 1;
+
+@interface FIRMessagingSyncMessageManager()
+
+@property(nonatomic, readwrite, strong) FIRMessagingRmqManager *rmqManager;
+
+@end
+
+@implementation FIRMessagingSyncMessageManager
+
+- (instancetype)init {
+ FIRMessagingInvalidateInitializer();
+}
+
+- (instancetype)initWithRmqManager:(FIRMessagingRmqManager *)rmqManager {
+ _FIRMessagingDevAssert(rmqManager, @"Invalid nil rmq manager while initalizing sync message manager");
+ self = [super init];
+ if (self) {
+ _rmqManager = rmqManager;
+ }
+ return self;
+}
+
+- (void)removeExpiredSyncMessages {
+ NSError *error;
+ int deleteCount = [self.rmqManager deleteExpiredOrFinishedSyncMessages:&error];
+ if (error) {
+ FIRMessagingLoggerError(kFIRMessagingMessageCodeSyncMessageManager000,
+ @"Error while deleting expired sync messages %@", error);
+ } else if (deleteCount > 0) {
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeSyncMessageManager001,
+ @"Successfully deleted %d sync messages from store", deleteCount);
+ }
+}
+
+- (BOOL)didReceiveAPNSSyncMessage:(NSDictionary *)message {
+ return [self didReceiveSyncMessage:message viaAPNS:YES viaMCS:NO];
+}
+
+- (BOOL)didReceiveMCSSyncMessage:(NSDictionary *)message {
+ return [self didReceiveSyncMessage:message viaAPNS:NO viaMCS:YES];
+}
+
+- (BOOL)didReceiveSyncMessage:(NSDictionary *)message
+ viaAPNS:(BOOL)viaAPNS
+ viaMCS:(BOOL)viaMCS {
+ NSString *rmqID = message[kFIRMessagingMessageIDKey];
+ _FIRMessagingDevAssert([rmqID length], @"Invalid nil rmqID for message");
+ if (![rmqID length]) {
+ FIRMessagingLoggerError(kFIRMessagingMessageCodeSyncMessageManager002,
+ @"Invalid nil rmqID for sync message.");
+ return NO;
+ }
+
+ FIRMessagingPersistentSyncMessage *persistentMessage =
+ [self.rmqManager querySyncMessageWithRmqID:rmqID];
+
+ NSError *error;
+ if (!persistentMessage) {
+
+ // Do not persist the new message if we don't have enough disk space
+ uint64_t freeDiskSpace = FIRMessagingGetFreeDiskSpaceInMB();
+ if (freeDiskSpace < kMinFreeDiskSpaceInMB) {
+ return NO;
+ }
+
+ int64_t expirationTime = [[self class] expirationTimeForSyncMessage:message];
+ if (![self.rmqManager saveSyncMessageWithRmqID:rmqID
+ expirationTime:expirationTime
+ apnsReceived:viaAPNS
+ mcsReceived:viaMCS
+ error:&error]) {
+ FIRMessagingLoggerError(kFIRMessagingMessageCodeSyncMessageManager003,
+ @"Failed to save sync message with rmqID %@", rmqID);
+ } else {
+ FIRMessagingLoggerInfo(kFIRMessagingMessageCodeSyncMessageManager004,
+ @"Added sync message to cache: %@", rmqID);
+ }
+ return NO;
+ }
+
+ if (viaAPNS && !persistentMessage.apnsReceived) {
+ persistentMessage.apnsReceived = YES;
+ if (![self.rmqManager updateSyncMessageViaAPNSWithRmqID:rmqID error:&error]) {
+ FIRMessagingLoggerError(kFIRMessagingMessageCodeSyncMessageManager005,
+ @"Failed to update APNS state for sync message %@", rmqID);
+ }
+ } else if (viaMCS && !persistentMessage.mcsReceived) {
+ persistentMessage.mcsReceived = YES;
+ if (![self.rmqManager updateSyncMessageViaMCSWithRmqID:rmqID error:&error]) {
+ FIRMessagingLoggerError(kFIRMessagingMessageCodeSyncMessageManager006,
+ @"Failed to update MCS state for sync message %@", rmqID);
+ }
+ }
+
+ // Received message via both ways we can safely delete it.
+ if (persistentMessage.apnsReceived && persistentMessage.mcsReceived) {
+ if (![self.rmqManager deleteSyncMessageWithRmqID:rmqID]) {
+ FIRMessagingLoggerError(kFIRMessagingMessageCodeSyncMessageManager007,
+ @"Failed to delete sync message %@", rmqID);
+ } else {
+ FIRMessagingLoggerInfo(kFIRMessagingMessageCodeSyncMessageManager008,
+ @"Successfully deleted sync message from cache %@", rmqID);
+ }
+ }
+
+ // Already received this message either via MCS or APNS.
+ return YES;
+}
+
++ (int64_t)expirationTimeForSyncMessage:(NSDictionary *)message {
+ int64_t ttl = kDefaultSyncMessageTTL;
+ if (message[kFIRMessagingMessageSyncMessageTTLKey]) {
+ ttl = [message[kFIRMessagingMessageSyncMessageTTLKey] longLongValue];
+ }
+ int64_t currentTime = FIRMessagingCurrentTimestampInSeconds();
+ return currentTime + ttl;
+}
+
+@end
diff --git a/Firebase/Messaging/FIRMessagingTopicOperation.h b/Firebase/Messaging/FIRMessagingTopicOperation.h
new file mode 100644
index 0000000..e4bbde8
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingTopicOperation.h
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+#import "FIRMessagingCheckinService.h"
+#import "FIRMessagingTopicsCommon.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * An asynchronous NSOperation subclass which performs a single network request for a topic
+ * subscription operation. Once completed, it calls its provided completion handler.
+ */
+@interface FIRMessagingTopicOperation : NSOperation
+
+@property(nonatomic, readonly, copy) NSString *topic;
+@property(nonatomic, readonly, assign) FIRMessagingTopicAction action;
+@property(nonatomic, readonly, copy) NSString *token;
+@property(nonatomic, readonly, copy, nullable) NSDictionary *options;
+@property(nonatomic, readonly, strong) FIRMessagingCheckinService *checkinService;
+
+- (instancetype)initWithTopic:(NSString *)topic
+ action:(FIRMessagingTopicAction)action
+ token:(NSString *)token
+ options:(nullable NSDictionary *)options
+ checkinService:(FIRMessagingCheckinService *)checkinService
+ completion:(FIRMessagingTopicOperationCompletion)completion;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Messaging/FIRMessagingTopicOperation.m b/Firebase/Messaging/FIRMessagingTopicOperation.m
new file mode 100644
index 0000000..955c4a6
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingTopicOperation.m
@@ -0,0 +1,246 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRMessagingTopicOperation.h"
+
+#import "FIRMessagingCheckinService.h"
+#import "FIRMessagingDefines.h"
+#import "FIRMessagingLogger.h"
+#import "FIRMessagingUtilities.h"
+#import "NSError+FIRMessaging.h"
+
+#define DEBUG_LOG_SUBSCRIPTION_OPERATION_DURATIONS 0
+
+static NSString *const kFIRMessagingSubscribeServerHost =
+ @"https://iid.googleapis.com/iid/register";
+
+NSString *FIRMessagingSubscriptionsServer() {
+ static NSString *serverHost = nil;
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ NSDictionary *environment = [[NSProcessInfo processInfo] environment];
+ NSString *customServerHost = environment[@"FCM_SERVER_ENDPOINT"];
+ if (customServerHost.length) {
+ serverHost = customServerHost;
+ } else {
+ serverHost = kFIRMessagingSubscribeServerHost;
+ }
+ });
+ return serverHost;
+}
+
+@interface FIRMessagingTopicOperation () {
+ BOOL _isFinished;
+ BOOL _isExecuting;
+}
+
+@property(nonatomic, readwrite, copy) NSString *topic;
+@property(nonatomic, readwrite, assign) FIRMessagingTopicAction action;
+@property(nonatomic, readwrite, copy) NSString *token;
+@property(nonatomic, readwrite, copy) NSDictionary *options;
+@property(nonatomic, readwrite, strong) FIRMessagingCheckinService *checkinService;
+@property(nonatomic, readwrite, copy) FIRMessagingTopicOperationCompletion completion;
+
+@property(atomic, strong) NSURLSessionDataTask *dataTask;
+
+@end
+
+@implementation FIRMessagingTopicOperation
+
++ (NSURLSession *)sharedSession {
+ static NSURLSession *subscriptionOperationSharedSession;
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
+ config.timeoutIntervalForResource = 60.0f; // 1 minute
+ subscriptionOperationSharedSession = [NSURLSession sessionWithConfiguration:config];
+ subscriptionOperationSharedSession.sessionDescription = @"com.google.fcm.topics.session";
+ });
+ return subscriptionOperationSharedSession;
+}
+
+- (instancetype)initWithTopic:(NSString *)topic
+ action:(FIRMessagingTopicAction)action
+ token:(NSString *)token
+ options:(NSDictionary *)options
+ checkinService:(FIRMessagingCheckinService *)checkinService
+ completion:(FIRMessagingTopicOperationCompletion)completion {
+ if (self = [super init]) {
+ _topic = topic;
+ _action = action;
+ _token = token;
+ _checkinService = checkinService;
+ _completion = completion;
+
+ _isExecuting = NO;
+ _isFinished = NO;
+ }
+ return self;
+}
+
+- (void)dealloc {
+ _topic = nil;
+ _token = nil;
+ _checkinService = nil;
+ _completion = nil;
+}
+
+- (BOOL)isAsynchronous {
+ return YES;
+}
+
+- (BOOL)isExecuting {
+ return _isExecuting;
+}
+
+- (void)setExecuting:(BOOL)executing {
+ [self willChangeValueForKey:@"isExecuting"];
+ _isExecuting = executing;
+ [self didChangeValueForKey:@"isExecuting"];
+}
+
+- (BOOL)isFinished {
+ return _isFinished;
+}
+
+- (void)setFinished:(BOOL)finished {
+ [self willChangeValueForKey:@"isFinished"];
+ _isFinished = finished;
+ [self didChangeValueForKey:@"isFinished"];
+}
+
+- (void)start {
+ if (self.isCancelled) {
+ [self finishWithResult:FIRMessagingTopicOperationResultCancelled error:nil];
+ return;
+ }
+
+ [self setExecuting:YES];
+
+ [self performSubscriptionChange];
+}
+
+- (void)finishWithResult:(FIRMessagingTopicOperationResult)result error:(NSError *)error {
+ // Add a check to prevent this finish from being called more than once.
+ if (self.isFinished) {
+ return;
+ }
+ self.dataTask = nil;
+ if (self.completion) {
+ self.completion(result, error);
+ }
+
+ [self setExecuting:NO];
+ [self setFinished:YES];
+}
+
+- (void)cancel {
+ [super cancel];
+ [self.dataTask cancel];
+ [self finishWithResult:FIRMessagingTopicOperationResultCancelled error:nil];
+}
+
+- (void)performSubscriptionChange {
+
+ NSURL *url = [NSURL URLWithString:FIRMessagingSubscriptionsServer()];
+ NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
+ NSString *appIdentifier = FIRMessagingAppIdentifier();
+ NSString *deviceAuthID = self.checkinService.deviceAuthID;
+ NSString *secretToken = self.checkinService.secretToken;
+ NSString *authString = [NSString stringWithFormat:@"AidLogin %@:%@", deviceAuthID, secretToken];
+ [request setValue:authString forHTTPHeaderField:@"Authorization"];
+ [request setValue:appIdentifier forHTTPHeaderField:@"app"];
+ [request setValue:self.checkinService.versionInfo forHTTPHeaderField:@"info"];
+
+ NSMutableString *content = [NSMutableString stringWithFormat:
+ @"sender=%@&app=%@&device=%@&"
+ @"app_ver=%@&X-gcm.topic=%@&X-scope=%@",
+ self.token,
+ appIdentifier,
+ deviceAuthID,
+ FIRMessagingCurrentAppVersion(),
+ self.topic,
+ self.topic];
+
+ if (self.action == FIRMessagingTopicActionUnsubscribe) {
+ [content appendString:@"&delete=true"];
+ }
+
+ FIRMessagingLoggerInfo(kFIRMessagingMessageCodeTopicOption000, @"Topic subscription request: %@",
+ content);
+
+ request.HTTPBody = [content dataUsingEncoding:NSUTF8StringEncoding];
+ [request setHTTPMethod:@"POST"];
+
+#if DEBUG_LOG_SUBSCRIPTION_OPERATION_DURATIONS
+ NSDate *start = [NSDate date];
+#endif
+
+ FIRMessaging_WEAKIFY(self)
+ void(^requestHandler)(NSData *, NSURLResponse *, NSError *) =
+ ^(NSData *data, NSURLResponse *URLResponse, NSError *error) {
+ FIRMessaging_STRONGIFY(self)
+ if (error) {
+ // Our operation could have been cancelled, which would result in our data task's error being
+ // NSURLErrorCancelled
+ if (error.code == NSURLErrorCancelled) {
+ // We would only have been cancelled in the -cancel method, which will call finish for us
+ // so just return and do nothing.
+ return;
+ }
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeTopicOption001,
+ @"Device registration HTTP fetch error. Error Code: %ld",
+ _FIRMessaging_L(error.code));
+ [self finishWithResult:FIRMessagingTopicOperationResultError error:error];
+ return;
+ }
+ NSString *response = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
+ if (response.length == 0) {
+ [self finishWithResult:FIRMessagingTopicOperationResultError
+ error:[NSError errorWithFCMErrorCode:kFIRMessagingErrorCodeUnknown]];
+ return;
+ }
+ NSArray *parts = [response componentsSeparatedByString:@"="];
+ _FIRMessagingDevAssert(parts.count, @"Invalid registration response");
+ if (![parts[0] isEqualToString:@"token"] || parts.count <= 1) {
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeTopicOption002,
+ @"Invalid registration request, response");
+ [self finishWithResult:FIRMessagingTopicOperationResultError
+ error:[NSError errorWithFCMErrorCode:kFIRMessagingErrorCodeUnknown]];
+ return;
+ }
+#if DEBUG_LOG_SUBSCRIPTION_OPERATION_DURATIONS
+ NSTimeInterval duration = -[start timeIntervalSinceNow];
+ FIRMessagingLoggerDebug(@"%@ change took %.2fs", self.topic, duration);
+#endif
+ [self finishWithResult:FIRMessagingTopicOperationResultSucceeded error:nil];
+
+ };
+
+ NSURLSession *urlSession = [FIRMessagingTopicOperation sharedSession];
+
+ self.dataTask = [urlSession dataTaskWithRequest:request completionHandler:requestHandler];
+ NSString *description;
+ if (_action == FIRMessagingTopicActionSubscribe) {
+ description = [NSString stringWithFormat:@"com.google.fcm.topics.subscribe: %@", _topic];
+ } else {
+ description = [NSString stringWithFormat:@"com.google.fcm.topics.unsubscribe: %@", _topic];
+ }
+ self.dataTask.taskDescription = description;
+ [self.dataTask resume];
+}
+
+@end
diff --git a/Firebase/Messaging/FIRMessagingTopicsCommon.h b/Firebase/Messaging/FIRMessagingTopicsCommon.h
new file mode 100644
index 0000000..50d8906
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingTopicsCommon.h
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * Represents the action taken on a subscription topic.
+ */
+typedef NS_ENUM(NSInteger, FIRMessagingTopicAction) {
+ FIRMessagingTopicActionSubscribe,
+ FIRMessagingTopicActionUnsubscribe
+};
+
+/**
+ * Represents the possible results of a topic operation.
+ */
+typedef NS_ENUM(NSInteger, FIRMessagingTopicOperationResult) {
+ FIRMessagingTopicOperationResultSucceeded,
+ FIRMessagingTopicOperationResultError,
+ FIRMessagingTopicOperationResultCancelled,
+};
+
+/**
+ * Callback to invoke once the HTTP call to FIRMessaging backend for updating
+ * subscription finishes.
+ *
+ * @param result The result of the operation. If the result is
+ * FIRMessagingTopicOperationResultError, the error parameter will be
+ * non-nil.
+ * @param error The error which occurred while updating the subscription topic
+ * on the FIRMessaging server. This will be nil in case the operation
+ * was successful, or if the operation was cancelled.
+ */
+typedef void(^FIRMessagingTopicOperationCompletion)
+ (FIRMessagingTopicOperationResult result, NSError * _Nullable error);
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Messaging/FIRMessagingUtilities.h b/Firebase/Messaging/FIRMessagingUtilities.h
new file mode 100644
index 0000000..ed9dc83
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingUtilities.h
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+typedef NS_ENUM(int8_t, FIRMessagingProtoTag) {
+ kFIRMessagingProtoTagInvalid = -1,
+ kFIRMessagingProtoTagHeartbeatPing = 0,
+ kFIRMessagingProtoTagHeartbeatAck = 1,
+ kFIRMessagingProtoTagLoginRequest = 2,
+ kFIRMessagingProtoTagLoginResponse = 3,
+ kFIRMessagingProtoTagClose = 4,
+ kFIRMessagingProtoTagIqStanza = 7,
+ kFIRMessagingProtoTagDataMessageStanza = 8,
+};
+
+@class GPBMessage;
+
+#pragma mark - Protocol Buffers
+
+FOUNDATION_EXPORT FIRMessagingProtoTag FIRMessagingGetTagForProto(GPBMessage *protoClass);
+FOUNDATION_EXPORT Class FIRMessagingGetClassForTag(FIRMessagingProtoTag tag);
+
+#pragma mark - MCS
+
+FOUNDATION_EXPORT NSString *FIRMessagingGetRmq2Id(GPBMessage *proto);
+FOUNDATION_EXPORT void FIRMessagingSetRmq2Id(GPBMessage *proto, NSString *pID);
+FOUNDATION_EXPORT int FIRMessagingGetLastStreamId(GPBMessage *proto);
+FOUNDATION_EXPORT void FIRMessagingSetLastStreamId(GPBMessage *proto, int sid);
+
+#pragma mark - Time
+
+FOUNDATION_EXPORT int64_t FIRMessagingCurrentTimestampInSeconds();
+FOUNDATION_EXPORT int64_t FIRMessagingCurrentTimestampInMilliseconds();
+
+#pragma mark - App Info
+
+FOUNDATION_EXPORT NSString *FIRMessagingCurrentAppVersion();
+FOUNDATION_EXPORT NSString *FIRMessagingAppIdentifier();
+
+FOUNDATION_EXPORT uint64_t FIRMessagingGetFreeDiskSpaceInMB();
diff --git a/Firebase/Messaging/FIRMessagingUtilities.m b/Firebase/Messaging/FIRMessagingUtilities.m
new file mode 100644
index 0000000..13c7a05
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingUtilities.m
@@ -0,0 +1,173 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRMessagingUtilities.h"
+
+#import "Protos/GtalkCore.pbobjc.h"
+
+#import "FIRMessagingLogger.h"
+
+// Convert the macro to a string
+#define STR_EXPAND(x) #x
+#define STR(x) STR_EXPAND(x)
+
+static const uint64_t kBytesToMegabytesDivisor = 1024 * 1024LL;
+
+#pragma mark - Protocol Buffers
+
+FIRMessagingProtoTag FIRMessagingGetTagForProto(GPBMessage *proto) {
+ if ([proto isKindOfClass:[GtalkHeartbeatPing class]]) {
+ return kFIRMessagingProtoTagHeartbeatPing;
+ } else if ([proto isKindOfClass:[GtalkHeartbeatAck class]]) {
+ return kFIRMessagingProtoTagHeartbeatAck;
+ } else if ([proto isKindOfClass:[GtalkLoginRequest class]]) {
+ return kFIRMessagingProtoTagLoginRequest;
+ } else if ([proto isKindOfClass:[GtalkLoginResponse class]]) {
+ return kFIRMessagingProtoTagLoginResponse;
+ } else if ([proto isKindOfClass:[GtalkClose class]]) {
+ return kFIRMessagingProtoTagClose;
+ } else if ([proto isKindOfClass:[GtalkIqStanza class]]) {
+ return kFIRMessagingProtoTagIqStanza;
+ } else if ([proto isKindOfClass:[GtalkDataMessageStanza class]]) {
+ return kFIRMessagingProtoTagDataMessageStanza;
+ }
+ return kFIRMessagingProtoTagInvalid;
+}
+
+Class FIRMessagingGetClassForTag(FIRMessagingProtoTag tag) {
+ switch (tag) {
+ case kFIRMessagingProtoTagHeartbeatPing:
+ return GtalkHeartbeatPing.class;
+ case kFIRMessagingProtoTagHeartbeatAck:
+ return GtalkHeartbeatAck.class;
+ case kFIRMessagingProtoTagLoginRequest:
+ return GtalkLoginRequest.class;
+ case kFIRMessagingProtoTagLoginResponse:
+ return GtalkLoginResponse.class;
+ case kFIRMessagingProtoTagClose:
+ return GtalkClose.class;
+ case kFIRMessagingProtoTagIqStanza:
+ return GtalkIqStanza.class;
+ case kFIRMessagingProtoTagDataMessageStanza:
+ return GtalkDataMessageStanza.class;
+ case kFIRMessagingProtoTagInvalid:
+ return NSNull.class;
+ }
+ return NSNull.class;
+}
+
+#pragma mark - MCS
+
+NSString *FIRMessagingGetRmq2Id(GPBMessage *proto) {
+ if ([proto isKindOfClass:[GtalkIqStanza class]]) {
+ if (((GtalkIqStanza *)proto).hasPersistentId) {
+ return ((GtalkIqStanza *)proto).persistentId;
+ }
+ } else if ([proto isKindOfClass:[GtalkDataMessageStanza class]]) {
+ if (((GtalkDataMessageStanza *)proto).hasPersistentId) {
+ return ((GtalkDataMessageStanza *)proto).persistentId;
+ }
+ }
+ return nil;
+}
+
+void FIRMessagingSetRmq2Id(GPBMessage *proto, NSString *pID) {
+ if ([proto isKindOfClass:[GtalkIqStanza class]]) {
+ ((GtalkIqStanza *)proto).persistentId = pID;
+ } else if ([proto isKindOfClass:[GtalkDataMessageStanza class]]) {
+ ((GtalkDataMessageStanza *)proto).persistentId = pID;
+ }
+}
+
+int FIRMessagingGetLastStreamId(GPBMessage *proto) {
+ if ([proto isKindOfClass:[GtalkIqStanza class]]) {
+ if (((GtalkIqStanza *)proto).hasLastStreamIdReceived) {
+ return ((GtalkIqStanza *)proto).lastStreamIdReceived;
+ }
+ } else if ([proto isKindOfClass:[GtalkDataMessageStanza class]]) {
+ if (((GtalkDataMessageStanza *)proto).hasLastStreamIdReceived) {
+ return ((GtalkDataMessageStanza *)proto).lastStreamIdReceived;
+ }
+ } else if ([proto isKindOfClass:[GtalkHeartbeatPing class]]) {
+ if (((GtalkHeartbeatPing *)proto).hasLastStreamIdReceived) {
+ return ((GtalkHeartbeatPing *)proto).lastStreamIdReceived;
+ }
+ } else if ([proto isKindOfClass:[GtalkHeartbeatAck class]]) {
+ if (((GtalkHeartbeatAck *)proto).hasLastStreamIdReceived) {
+ return ((GtalkHeartbeatAck *)proto).lastStreamIdReceived;
+ }
+ }
+ return -1;
+}
+
+void FIRMessagingSetLastStreamId(GPBMessage *proto, int sid) {
+ if ([proto isKindOfClass:[GtalkIqStanza class]]) {
+ ((GtalkIqStanza *)proto).lastStreamIdReceived = sid;
+ } else if ([proto isKindOfClass:[GtalkDataMessageStanza class]]) {
+ ((GtalkDataMessageStanza *)proto).lastStreamIdReceived = sid;
+ } else if ([proto isKindOfClass:[GtalkHeartbeatPing class]]) {
+ ((GtalkHeartbeatPing *)proto).lastStreamIdReceived = sid;
+ } else if ([proto isKindOfClass:[GtalkHeartbeatAck class]]) {
+ ((GtalkHeartbeatAck *)proto).lastStreamIdReceived = sid;
+ }
+}
+
+#pragma mark - Time
+
+int64_t FIRMessagingCurrentTimestampInSeconds() {
+ return (int64_t)[[NSDate date] timeIntervalSince1970];
+}
+
+int64_t FIRMessagingCurrentTimestampInMilliseconds() {
+ return (int64_t)(FIRMessagingCurrentTimestampInSeconds() * 1000.0);
+}
+
+#pragma mark - App Info
+
+NSString *FIRMessagingCurrentAppVersion() {
+ NSString *version = [[NSBundle mainBundle] infoDictionary][@"CFBundleShortVersionString"];
+ if (![version length]) {
+ FIRMessagingLoggerError(kFIRMessagingMessageCodeUtilities000,
+ @"Could not find current app version");
+ return @"";
+ }
+ return version;
+}
+
+NSString *FIRMessagingAppIdentifier() {
+ return [[NSBundle mainBundle] bundleIdentifier];
+}
+
+uint64_t FIRMessagingGetFreeDiskSpaceInMB() {
+ NSError *error;
+ NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
+
+ NSDictionary *attributesMap =
+ [[NSFileManager defaultManager] attributesOfFileSystemForPath:[paths lastObject]
+ error:&error];
+ if (attributesMap) {
+ uint64_t totalSizeInBytes __unused = [attributesMap[NSFileSystemSize] longLongValue];
+ uint64_t freeSizeInBytes = [attributesMap[NSFileSystemFreeSize] longLongValue];
+ FIRMessagingLoggerDebug(
+ kFIRMessagingMessageCodeUtilities001, @"Device has capacity %llu MB with %llu MB free.",
+ totalSizeInBytes / kBytesToMegabytesDivisor, freeSizeInBytes / kBytesToMegabytesDivisor);
+ return ((double)freeSizeInBytes) / kBytesToMegabytesDivisor;
+ } else {
+ FIRMessagingLoggerError(kFIRMessagingMessageCodeUtilities002,
+ @"Error in retreiving device's free memory %@", error);
+ return 0;
+ }
+}
diff --git a/Firebase/Messaging/FIRMessagingVersionUtilities.h b/Firebase/Messaging/FIRMessagingVersionUtilities.h
new file mode 100644
index 0000000..df7cebe
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingVersionUtilities.h
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+/**
+ * Parsing utility for FIRMessaging Library versions. FIRMessaging Library follows semantic versioning.
+ * This provides utilities to parse the library versions to enable features and do
+ * updates based on appropriate library versions.
+ *
+ * Some example semantic versions are 1.0.1, 2.1.0, 2.1.1, 2.2.0-alpha1, 2.2.1-beta1
+ */
+
+FOUNDATION_EXPORT NSString *FIRMessagingCurrentLibraryVersion();
+/// Returns the current Major version of FIRMessaging library.
+FOUNDATION_EXPORT int FIRMessagingCurrentLibraryVersionMajor();
+/// Returns the current Minor version of FIRMessaging library.
+FOUNDATION_EXPORT int FIRMessagingCurrentLibraryVersionMinor();
+/// Returns the current Patch version of FIRMessaging library.
+FOUNDATION_EXPORT int FIRMessagingCurrentLibraryVersionPatch();
+/// Returns YES if current library version is `beta` else NO.
+FOUNDATION_EXPORT BOOL FIRMessagingCurrentLibraryVersionIsBeta();
diff --git a/Firebase/Messaging/FIRMessagingVersionUtilities.m b/Firebase/Messaging/FIRMessagingVersionUtilities.m
new file mode 100644
index 0000000..e0f922c
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingVersionUtilities.m
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRMessagingVersionUtilities.h"
+
+#import "FIRMessagingDefines.h"
+
+// Convert the macro to a string
+#define STR_EXPAND(x) #x
+#define STR(x) STR_EXPAND(x)
+
+static NSString *const kSemanticVersioningSeparator = @".";
+static NSString *const kBetaVersionPrefix = @"-beta";
+
+static NSString *libraryVersion;
+static int majorVersion;
+static int minorVersion;
+static int patchVersion;
+static int betaVersion;
+
+void FIRMessagingParseCurrentLibraryVersion() {
+ static NSArray *allVersions;
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ NSMutableString *daylightVersion = [NSMutableString stringWithUTF8String:STR(FIRMessaging_LIB_VERSION)];
+ // Parse versions
+ // major, minor, patch[-beta#]
+ allVersions = [daylightVersion componentsSeparatedByString:kSemanticVersioningSeparator];
+ _FIRMessagingDevAssert(allVersions.count == 3, @"Invalid versioning of FIRMessaging library");
+ if (allVersions.count == 3) {
+ majorVersion = [allVersions[0] intValue];
+ minorVersion = [allVersions[1] intValue];
+
+ // Parse patch and beta versions
+ NSArray *patchAndBetaVersion =
+ [allVersions[2] componentsSeparatedByString:kBetaVersionPrefix];
+ _FIRMessagingDevAssert(patchAndBetaVersion.count <= 2, @"Invalid versioning of FIRMessaging library");
+ if (patchAndBetaVersion.count == 2) {
+ patchVersion = [patchAndBetaVersion[0] intValue];
+ betaVersion = [patchAndBetaVersion[1] intValue];
+ } else if (patchAndBetaVersion.count == 1) {
+ patchVersion = [patchAndBetaVersion[0] intValue];
+ }
+ }
+
+ // Copy library version
+ libraryVersion = [daylightVersion copy];
+ });
+}
+
+NSString *FIRMessagingCurrentLibraryVersion() {
+ FIRMessagingParseCurrentLibraryVersion();
+ return libraryVersion;
+}
+
+int FIRMessagingCurrentLibraryVersionMajor() {
+ FIRMessagingParseCurrentLibraryVersion();
+ return majorVersion;
+}
+
+int FIRMessagingCurrentLibraryVersionMinor() {
+ FIRMessagingParseCurrentLibraryVersion();
+ return minorVersion;
+}
+
+int FIRMessagingCurrentLibraryVersionPatch() {
+ FIRMessagingParseCurrentLibraryVersion();
+ return patchVersion;
+}
+
+BOOL FIRMessagingCurrentLibraryVersionIsBeta() {
+ FIRMessagingParseCurrentLibraryVersion();
+ return betaVersion > 0;
+}
diff --git a/Firebase/Messaging/FIRMessaging_Private.h b/Firebase/Messaging/FIRMessaging_Private.h
new file mode 100644
index 0000000..0c35179
--- /dev/null
+++ b/Firebase/Messaging/FIRMessaging_Private.h
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRMessaging.h"
+
+@class FIRMessagingClient;
+@class FIRMessagingPubSub;
+
+typedef NS_ENUM(int8_t, FIRMessagingNetworkStatus) {
+ kFIRMessagingReachabilityNotReachable = 0,
+ kFIRMessagingReachabilityReachableViaWiFi,
+ kFIRMessagingReachabilityReachableViaWWAN,
+};
+
+@interface FIRMessagingRemoteMessage ()
+
+@property(nonatomic, strong) NSDictionary *appData;
+
+@end
+
+@interface FIRMessaging ()
+
+#pragma mark - Private API
+
+- (NSString *)defaultFcmToken;
+- (FIRMessagingClient *)client;
+- (FIRMessagingPubSub *)pubsub;
+
+// Create a sample message to be sent over the wire using FIRMessaging. Look at
+// FIRMessagingService.h to see what each param signifies.
++ (NSMutableDictionary *)createFIRMessagingMessageWithMessage:(NSDictionary *)message
+ to:(NSString *)to
+ withID:(NSString *)msgID
+ timeToLive:(int64_t)ttl
+ delay:(int)delay;
+
+- (BOOL)isNetworkAvailable;
+- (FIRMessagingNetworkStatus)networkType;
+
+// Set the APNS token for FCM.
+- (void)setAPNSToken:(NSData *)apnsToken error:(NSError *)error;
+
+@end
diff --git a/Firebase/Messaging/FirebaseMessaging.h b/Firebase/Messaging/FirebaseMessaging.h
new file mode 100644
index 0000000..ef081c9
--- /dev/null
+++ b/Firebase/Messaging/FirebaseMessaging.h
@@ -0,0 +1,17 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRMessaging.h"
diff --git a/Firebase/Messaging/FirebaseMessaging.podspec b/Firebase/Messaging/FirebaseMessaging.podspec
new file mode 100644
index 0000000..4bcbebb
--- /dev/null
+++ b/Firebase/Messaging/FirebaseMessaging.podspec
@@ -0,0 +1,41 @@
+# This podspec is not intended to be deployed. It is solely for the static
+# library framework build process at
+# https://github.com/firebase/firebase-ios-sdk/tree/master/BuildFrameworks
+
+Pod::Spec.new do |s|
+ s.name = 'FirebaseMessaging'
+ s.version = '2.0.0'
+ s.summary = 'Firebase Open Source Libraries for iOS.'
+
+ s.description = <<-DESC
+Simplify your iOS development, grow your user base, and monetize more effectively with Firebase.
+ DESC
+
+ s.homepage = 'https://firebase.google.com'
+ s.license = { :type => 'Apache', :file => '../../LICENSE' }
+ s.authors = 'Google, Inc.'
+
+ # NOTE that the FirebaseDev pod is neither publicly deployed nor yet interchangeable with the
+ # Firebase pod
+ s.source = { :git => 'https://github.com/firebase/firebase-ios-sdk.git', :tag => s.version.to_s }
+ s.social_media_url = 'https://twitter.com/Firebase'
+ s.ios.deployment_target = '7.0'
+
+ s.source_files = '**/*.[mh]'
+ s.requires_arc = '*.m'
+ s.public_header_files =
+ 'Public/FirebaseMessaging.h',
+ 'Public/FIRMessaging.h'
+
+ s.library = 'sqlite3'
+ s.xcconfig = { 'GCC_PREPROCESSOR_DEFINITIONS' =>
+ '$(inherited) ' +
+ 'GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS=1 ' +
+ 'FIRMessaging_LIB_VERSION=' + String(s.version)
+ }
+ s.framework = 'AddressBook'
+ s.framework = 'SystemConfiguration'
+# s.dependency 'FirebaseDev/Core'
+ s.dependency 'GoogleToolboxForMac/Logger', '~> 2.1'
+ s.dependency 'Protobuf', '~> 3.1'
+end
diff --git a/Firebase/Messaging/InternalHeaders/FIRMessagingInternalUtilities.h b/Firebase/Messaging/InternalHeaders/FIRMessagingInternalUtilities.h
new file mode 100644
index 0000000..d6a1639
--- /dev/null
+++ b/Firebase/Messaging/InternalHeaders/FIRMessagingInternalUtilities.h
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/// @file FIRMessagingInternalUtilities.h
+///
+/// Internal Class Names and Methods that other libs can query at runtime.
+
+/// FIRMessaging Class that responds to the FIRMessaging SDK version selector.
+/// Verify at runtime if the class exists and implements the
+/// required method.
+static NSString *const kFIRMessagingSDKClassString = @"FIRMessaging";
+
+/// FIRMessaging selector that returns the current FIRMessaging library version.
+static NSString *const kFIRMessagingSDKVersionSelectorString = @"FIRMessagingSDKVersion";
+
+/// FIRMessaging selector that returns the current device locale.
+static NSString *const kFIRMessagingSDKLocaleSelectorString = @"FIRMessagingSDKCurrentLocale";
diff --git a/Firebase/Messaging/NSDictionary+FIRMessaging.h b/Firebase/Messaging/NSDictionary+FIRMessaging.h
new file mode 100644
index 0000000..fe14451
--- /dev/null
+++ b/Firebase/Messaging/NSDictionary+FIRMessaging.h
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+@interface NSDictionary (FIRMessaging)
+
+/**
+ * Returns a string representation for the given dictionary. Assumes that all
+ * keys and values are strings.
+ *
+ * @return A string representation of all keys and values in the dictionary.
+ * The returned string is not pretty-printed.
+ */
+- (NSString *)fcm_string;
+
+/**
+ * Check if the dictionary has any non-string keys or values.
+ *
+ * @return YES if the dictionary has any non-string keys or values else NO.
+ */
+- (BOOL)fcm_hasNonStringKeysOrValues;
+
+/**
+ * Trims all (key, value) pair in a dictionary that are not strings.
+ *
+ * @return A new copied dictionary with all the non-string keys or values
+ * removed from the original dictionary.
+ */
+- (NSDictionary *)fcm_trimNonStringValues;
+
+@end
diff --git a/Firebase/Messaging/NSDictionary+FIRMessaging.m b/Firebase/Messaging/NSDictionary+FIRMessaging.m
new file mode 100644
index 0000000..8df22ab
--- /dev/null
+++ b/Firebase/Messaging/NSDictionary+FIRMessaging.m
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "NSDictionary+FIRMessaging.h"
+
+@implementation NSDictionary (FIRMessaging)
+
+- (NSString *)fcm_string {
+ NSMutableString *dictAsString = [NSMutableString string];
+ NSString *separator = @"|";
+ for (id key in self) {
+ id value = self[key];
+ if ([key isKindOfClass:[NSString class]] && [value isKindOfClass:[NSString class]]) {
+ [dictAsString appendFormat:@"%@:%@%@", key, value, separator];
+ }
+ }
+ // remove the last separator
+ if ([dictAsString length]) {
+ [dictAsString deleteCharactersInRange:NSMakeRange(dictAsString.length - 1, 1)];
+ }
+ return [dictAsString copy];
+}
+
+- (BOOL)fcm_hasNonStringKeysOrValues {
+ for (id key in self) {
+ id value = self[key];
+ if (![key isKindOfClass:[NSString class]] || ![value isKindOfClass:[NSString class]]) {
+ return YES;
+ }
+ }
+ return NO;
+}
+
+- (NSDictionary *)fcm_trimNonStringValues {
+ NSMutableDictionary *trimDictionary =
+ [NSMutableDictionary dictionaryWithCapacity:self.count];
+ for (id key in self) {
+ id value = self[key];
+ if ([key isKindOfClass:[NSString class]] && [value isKindOfClass:[NSString class]]) {
+ trimDictionary[(NSString *)key] = value;
+ }
+ }
+ return trimDictionary;
+}
+
+@end
diff --git a/Firebase/Messaging/NSError+FIRMessaging.h b/Firebase/Messaging/NSError+FIRMessaging.h
new file mode 100644
index 0000000..9b1e214
--- /dev/null
+++ b/Firebase/Messaging/NSError+FIRMessaging.h
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+FOUNDATION_EXPORT NSString *const kFIRMessagingDomain;
+
+typedef NS_ENUM(NSUInteger, FIRMessagingInternalErrorCode) {
+ // Unknown error.
+ kFIRMessagingErrorCodeUnknown = 0,
+
+ // HTTP related errors.
+ kFIRMessagingErrorCodeAuthentication = 1,
+ kFIRMessagingErrorCodeNoAccess = 2,
+ kFIRMessagingErrorCodeTimeout = 3,
+ kFIRMessagingErrorCodeNetwork = 4,
+
+ // Another operation is in progress.
+ kFIRMessagingErrorCodeOperationInProgress = 5,
+
+ // Failed to perform device check in.
+ kFIRMessagingErrorCodeRegistrarFailedToCheckIn = 6,
+
+ kFIRMessagingErrorCodeInvalidRequest = 7,
+
+ // FIRMessaging generic errors
+ kFIRMessagingErrorCodeMissingDeviceID = 501,
+
+ // upstream send errors
+ kFIRMessagingErrorServiceNotAvailable = 1001,
+ kFIRMessagingErrorInvalidParameters = 1002,
+ kFIRMessagingErrorMissingTo = 1003,
+ kFIRMessagingErrorSave = 1004,
+ kFIRMessagingErrorSizeExceeded = 1005,
+ // Future Send Errors
+
+ // MCS errors
+ // Already connected with MCS
+ kFIRMessagingErrorCodeAlreadyConnected = 2001,
+
+ // PubSub errors
+ kFIRMessagingErrorCodePubSubAlreadySubscribed = 3001,
+ kFIRMessagingErrorCodePubSubAlreadyUnsubscribed = 3002,
+ kFIRMessagingErrorCodePubSubInvalidTopic = 3003,
+ kFIRMessagingErrorCodePubSubFIRMessagingNotSetup = 3004,
+};
+
+@interface NSError (FIRMessaging)
+
+@property(nonatomic, readonly) FIRMessagingInternalErrorCode fcmErrorCode;
+
++ (NSError *)errorWithFCMErrorCode:(FIRMessagingInternalErrorCode)fcmErrorCode;
++ (NSError *)fcm_errorWithCode:(NSInteger)code userInfo:(NSDictionary *)userInfo;
+
+@end
diff --git a/Firebase/Messaging/NSError+FIRMessaging.m b/Firebase/Messaging/NSError+FIRMessaging.m
new file mode 100644
index 0000000..e4b8736
--- /dev/null
+++ b/Firebase/Messaging/NSError+FIRMessaging.m
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "NSError+FIRMessaging.h"
+
+NSString *const kFIRMessagingDomain = @"com.google.fcm";
+
+@implementation NSError (FIRMessaging)
+
+- (FIRMessagingInternalErrorCode)fcmErrorCode {
+ return (FIRMessagingInternalErrorCode)self.code;
+}
+
++ (NSError *)errorWithFCMErrorCode:(FIRMessagingInternalErrorCode)fcmErrorCode {
+ return [NSError errorWithDomain:kFIRMessagingDomain code:fcmErrorCode userInfo:nil];
+}
+
++ (NSError *)fcm_errorWithCode:(NSInteger)code userInfo:(NSDictionary *)userInfo {
+ return [NSError errorWithDomain:kFIRMessagingDomain code:code userInfo:userInfo];
+}
+
+@end
diff --git a/Firebase/Messaging/Protos/GtalkCore.pbobjc.h b/Firebase/Messaging/Protos/GtalkCore.pbobjc.h
new file mode 100644
index 0000000..d4c8c8c
--- /dev/null
+++ b/Firebase/Messaging/Protos/GtalkCore.pbobjc.h
@@ -0,0 +1,1344 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Generated by the protocol buffer compiler. DO NOT EDIT!
+// source: buzz/mobile/proto/gtalk_core.proto
+
+// This CPP symbol can be defined to use imports that match up to the framework
+// imports needed when using CocoaPods.
+#if !defined(GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS)
+ #define GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS 0
+#endif
+
+#if GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS
+ #import <Protobuf/GPBProtocolBuffers.h>
+#else
+ #import "GPBProtocolBuffers.h"
+#endif
+
+#if GOOGLE_PROTOBUF_OBJC_VERSION < 30002
+#error This file was generated by a newer version of protoc which is incompatible with your Protocol Buffer library sources.
+#endif
+#if 30002 < GOOGLE_PROTOBUF_OBJC_MIN_SUPPORTED_VERSION
+#error This file was generated by an older version of protoc which is incompatible with your Protocol Buffer library sources.
+#endif
+
+// @@protoc_insertion_point(imports)
+
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdeprecated-declarations"
+
+CF_EXTERN_C_BEGIN
+
+@class GtalkAppData;
+@class GtalkCellTower;
+@class GtalkClientEvent;
+@class GtalkErrorInfo;
+@class GtalkExtension;
+@class GtalkHeartbeatConfig;
+@class GtalkHeartbeatStat;
+@class GtalkPresenceStanza;
+@class GtalkSetting;
+
+NS_ASSUME_NONNULL_BEGIN
+
+#pragma mark - Enum GtalkLoginRequest_AuthService
+
+typedef GPB_ENUM(GtalkLoginRequest_AuthService) {
+ GtalkLoginRequest_AuthService_Mail = 0,
+ GtalkLoginRequest_AuthService_AndroidCloudToDeviceMessage = 1,
+ GtalkLoginRequest_AuthService_AndroidId = 2,
+};
+
+GPBEnumDescriptor *GtalkLoginRequest_AuthService_EnumDescriptor(void);
+
+/**
+ * Checks to see if the given value is defined by the enum or was not known at
+ * the time this source was generated.
+ **/
+BOOL GtalkLoginRequest_AuthService_IsValidValue(int32_t value);
+
+#pragma mark - Enum GtalkMessageStanza_MessageType
+
+typedef GPB_ENUM(GtalkMessageStanza_MessageType) {
+ GtalkMessageStanza_MessageType_Normal = 0,
+ GtalkMessageStanza_MessageType_Chat = 1,
+ GtalkMessageStanza_MessageType_Groupchat = 2,
+ GtalkMessageStanza_MessageType_Headline = 3,
+ GtalkMessageStanza_MessageType_Error = 4,
+};
+
+GPBEnumDescriptor *GtalkMessageStanza_MessageType_EnumDescriptor(void);
+
+/**
+ * Checks to see if the given value is defined by the enum or was not known at
+ * the time this source was generated.
+ **/
+BOOL GtalkMessageStanza_MessageType_IsValidValue(int32_t value);
+
+#pragma mark - Enum GtalkPresenceStanza_PresenceType
+
+typedef GPB_ENUM(GtalkPresenceStanza_PresenceType) {
+ GtalkPresenceStanza_PresenceType_Unavailable = 0,
+ GtalkPresenceStanza_PresenceType_Subscribe = 1,
+ GtalkPresenceStanza_PresenceType_Subscribed = 2,
+ GtalkPresenceStanza_PresenceType_Unsubscribe = 3,
+ GtalkPresenceStanza_PresenceType_Unsubscribed = 4,
+ GtalkPresenceStanza_PresenceType_Probe = 5,
+ GtalkPresenceStanza_PresenceType_Error = 6,
+};
+
+GPBEnumDescriptor *GtalkPresenceStanza_PresenceType_EnumDescriptor(void);
+
+/**
+ * Checks to see if the given value is defined by the enum or was not known at
+ * the time this source was generated.
+ **/
+BOOL GtalkPresenceStanza_PresenceType_IsValidValue(int32_t value);
+
+#pragma mark - Enum GtalkPresenceStanza_ShowType
+
+typedef GPB_ENUM(GtalkPresenceStanza_ShowType) {
+ GtalkPresenceStanza_ShowType_Away = 0,
+ GtalkPresenceStanza_ShowType_Chat = 1,
+ GtalkPresenceStanza_ShowType_Dnd = 2,
+ GtalkPresenceStanza_ShowType_Xa = 3,
+};
+
+GPBEnumDescriptor *GtalkPresenceStanza_ShowType_EnumDescriptor(void);
+
+/**
+ * Checks to see if the given value is defined by the enum or was not known at
+ * the time this source was generated.
+ **/
+BOOL GtalkPresenceStanza_ShowType_IsValidValue(int32_t value);
+
+#pragma mark - Enum GtalkPresenceStanza_ClientType
+
+typedef GPB_ENUM(GtalkPresenceStanza_ClientType) {
+ GtalkPresenceStanza_ClientType_Mobile = 0,
+ GtalkPresenceStanza_ClientType_Android = 1,
+};
+
+GPBEnumDescriptor *GtalkPresenceStanza_ClientType_EnumDescriptor(void);
+
+/**
+ * Checks to see if the given value is defined by the enum or was not known at
+ * the time this source was generated.
+ **/
+BOOL GtalkPresenceStanza_ClientType_IsValidValue(int32_t value);
+
+#pragma mark - Enum GtalkPresenceStanza_CapabilitiesFlags
+
+typedef GPB_ENUM(GtalkPresenceStanza_CapabilitiesFlags) {
+ GtalkPresenceStanza_CapabilitiesFlags_HasVoiceV1 = 1,
+ GtalkPresenceStanza_CapabilitiesFlags_HasVideoV1 = 2,
+ GtalkPresenceStanza_CapabilitiesFlags_HasCameraV1 = 4,
+ GtalkPresenceStanza_CapabilitiesFlags_HasPmucV1 = 8,
+};
+
+GPBEnumDescriptor *GtalkPresenceStanza_CapabilitiesFlags_EnumDescriptor(void);
+
+/**
+ * Checks to see if the given value is defined by the enum or was not known at
+ * the time this source was generated.
+ **/
+BOOL GtalkPresenceStanza_CapabilitiesFlags_IsValidValue(int32_t value);
+
+#pragma mark - Enum GtalkBatchPresenceStanza_Type
+
+typedef GPB_ENUM(GtalkBatchPresenceStanza_Type) {
+ GtalkBatchPresenceStanza_Type_Get = 0,
+ GtalkBatchPresenceStanza_Type_Set = 1,
+};
+
+GPBEnumDescriptor *GtalkBatchPresenceStanza_Type_EnumDescriptor(void);
+
+/**
+ * Checks to see if the given value is defined by the enum or was not known at
+ * the time this source was generated.
+ **/
+BOOL GtalkBatchPresenceStanza_Type_IsValidValue(int32_t value);
+
+#pragma mark - Enum GtalkIqStanza_IqType
+
+typedef GPB_ENUM(GtalkIqStanza_IqType) {
+ GtalkIqStanza_IqType_Get = 0,
+ GtalkIqStanza_IqType_Set = 1,
+ GtalkIqStanza_IqType_Result = 2,
+ GtalkIqStanza_IqType_Error = 3,
+};
+
+GPBEnumDescriptor *GtalkIqStanza_IqType_EnumDescriptor(void);
+
+/**
+ * Checks to see if the given value is defined by the enum or was not known at
+ * the time this source was generated.
+ **/
+BOOL GtalkIqStanza_IqType_IsValidValue(int32_t value);
+
+#pragma mark - Enum GtalkClientEvent_Type
+
+typedef GPB_ENUM(GtalkClientEvent_Type) {
+ GtalkClientEvent_Type_Unknown = 0,
+ GtalkClientEvent_Type_DiscardedEvents = 1,
+ GtalkClientEvent_Type_FailedConnection = 2,
+ GtalkClientEvent_Type_SuccessfulConnection = 3,
+};
+
+GPBEnumDescriptor *GtalkClientEvent_Type_EnumDescriptor(void);
+
+/**
+ * Checks to see if the given value is defined by the enum or was not known at
+ * the time this source was generated.
+ **/
+BOOL GtalkClientEvent_Type_IsValidValue(int32_t value);
+
+#pragma mark - GtalkGtalkCoreRoot
+
+/**
+ * Exposes the extension registry for this file.
+ *
+ * The base class provides:
+ * @code
+ * + (GPBExtensionRegistry *)extensionRegistry;
+ * @endcode
+ * which is a @c GPBExtensionRegistry that includes all the extensions defined by
+ * this file and all files that it depends on.
+ **/
+@interface GtalkGtalkCoreRoot : GPBRootObject
+@end
+
+#pragma mark - GtalkHeartbeatPing
+
+typedef GPB_ENUM(GtalkHeartbeatPing_FieldNumber) {
+ GtalkHeartbeatPing_FieldNumber_StreamId = 1,
+ GtalkHeartbeatPing_FieldNumber_LastStreamIdReceived = 2,
+ GtalkHeartbeatPing_FieldNumber_Status = 3,
+ GtalkHeartbeatPing_FieldNumber_CellTower = 4,
+ GtalkHeartbeatPing_FieldNumber_IntervalMs = 5,
+};
+
+@interface GtalkHeartbeatPing : GPBMessage
+
+
+@property(nonatomic, readwrite) int32_t streamId;
+
+@property(nonatomic, readwrite) BOOL hasStreamId;
+
+@property(nonatomic, readwrite) int32_t lastStreamIdReceived;
+
+@property(nonatomic, readwrite) BOOL hasLastStreamIdReceived;
+
+@property(nonatomic, readwrite) int64_t status;
+
+@property(nonatomic, readwrite) BOOL hasStatus;
+
+@property(nonatomic, readwrite, strong, null_resettable) GtalkCellTower *cellTower;
+/** Test to see if @c cellTower has been set. */
+@property(nonatomic, readwrite) BOOL hasCellTower;
+
+
+@property(nonatomic, readwrite) int32_t intervalMs;
+
+@property(nonatomic, readwrite) BOOL hasIntervalMs;
+@end
+
+#pragma mark - GtalkHeartbeatAck
+
+typedef GPB_ENUM(GtalkHeartbeatAck_FieldNumber) {
+ GtalkHeartbeatAck_FieldNumber_StreamId = 1,
+ GtalkHeartbeatAck_FieldNumber_LastStreamIdReceived = 2,
+ GtalkHeartbeatAck_FieldNumber_Status = 3,
+ GtalkHeartbeatAck_FieldNumber_CellTower = 4,
+ GtalkHeartbeatAck_FieldNumber_IntervalMs = 5,
+};
+
+@interface GtalkHeartbeatAck : GPBMessage
+
+
+@property(nonatomic, readwrite) int32_t streamId;
+
+@property(nonatomic, readwrite) BOOL hasStreamId;
+
+@property(nonatomic, readwrite) int32_t lastStreamIdReceived;
+
+@property(nonatomic, readwrite) BOOL hasLastStreamIdReceived;
+
+@property(nonatomic, readwrite) int64_t status;
+
+@property(nonatomic, readwrite) BOOL hasStatus;
+
+@property(nonatomic, readwrite, strong, null_resettable) GtalkCellTower *cellTower;
+/** Test to see if @c cellTower has been set. */
+@property(nonatomic, readwrite) BOOL hasCellTower;
+
+
+@property(nonatomic, readwrite) int32_t intervalMs;
+
+@property(nonatomic, readwrite) BOOL hasIntervalMs;
+@end
+
+#pragma mark - GtalkErrorInfo
+
+typedef GPB_ENUM(GtalkErrorInfo_FieldNumber) {
+ GtalkErrorInfo_FieldNumber_Code = 1,
+ GtalkErrorInfo_FieldNumber_Message = 2,
+ GtalkErrorInfo_FieldNumber_Type = 3,
+ GtalkErrorInfo_FieldNumber_Extension = 4,
+};
+
+@interface GtalkErrorInfo : GPBMessage
+
+
+@property(nonatomic, readwrite) int32_t code;
+
+@property(nonatomic, readwrite) BOOL hasCode;
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *message;
+/** Test to see if @c message has been set. */
+@property(nonatomic, readwrite) BOOL hasMessage;
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *type;
+/** Test to see if @c type has been set. */
+@property(nonatomic, readwrite) BOOL hasType;
+
+
+@property(nonatomic, readwrite, strong, null_resettable) GtalkExtension *extension;
+/** Test to see if @c extension has been set. */
+@property(nonatomic, readwrite) BOOL hasExtension;
+
+@end
+
+#pragma mark - GtalkSetting
+
+typedef GPB_ENUM(GtalkSetting_FieldNumber) {
+ GtalkSetting_FieldNumber_Name = 1,
+ GtalkSetting_FieldNumber_Value = 2,
+};
+
+@interface GtalkSetting : GPBMessage
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *name;
+/** Test to see if @c name has been set. */
+@property(nonatomic, readwrite) BOOL hasName;
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *value;
+/** Test to see if @c value has been set. */
+@property(nonatomic, readwrite) BOOL hasValue;
+
+@end
+
+#pragma mark - GtalkHeartbeatStat
+
+typedef GPB_ENUM(GtalkHeartbeatStat_FieldNumber) {
+ GtalkHeartbeatStat_FieldNumber_Ip = 1,
+ GtalkHeartbeatStat_FieldNumber_Timeout = 2,
+ GtalkHeartbeatStat_FieldNumber_IntervalMs = 3,
+};
+
+@interface GtalkHeartbeatStat : GPBMessage
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *ip;
+/** Test to see if @c ip has been set. */
+@property(nonatomic, readwrite) BOOL hasIp;
+
+
+@property(nonatomic, readwrite) BOOL timeout;
+
+@property(nonatomic, readwrite) BOOL hasTimeout;
+
+@property(nonatomic, readwrite) int32_t intervalMs;
+
+@property(nonatomic, readwrite) BOOL hasIntervalMs;
+@end
+
+#pragma mark - GtalkHeartbeatConfig
+
+typedef GPB_ENUM(GtalkHeartbeatConfig_FieldNumber) {
+ GtalkHeartbeatConfig_FieldNumber_UploadStat = 1,
+ GtalkHeartbeatConfig_FieldNumber_Ip = 2,
+ GtalkHeartbeatConfig_FieldNumber_IntervalMs = 3,
+};
+
+@interface GtalkHeartbeatConfig : GPBMessage
+
+
+@property(nonatomic, readwrite) BOOL uploadStat;
+
+@property(nonatomic, readwrite) BOOL hasUploadStat;
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *ip;
+/** Test to see if @c ip has been set. */
+@property(nonatomic, readwrite) BOOL hasIp;
+
+
+@property(nonatomic, readwrite) int32_t intervalMs;
+
+@property(nonatomic, readwrite) BOOL hasIntervalMs;
+@end
+
+#pragma mark - GtalkLoginRequest
+
+typedef GPB_ENUM(GtalkLoginRequest_FieldNumber) {
+ GtalkLoginRequest_FieldNumber_Id_p = 1,
+ GtalkLoginRequest_FieldNumber_Domain = 2,
+ GtalkLoginRequest_FieldNumber_User = 3,
+ GtalkLoginRequest_FieldNumber_Resource = 4,
+ GtalkLoginRequest_FieldNumber_AuthToken = 5,
+ GtalkLoginRequest_FieldNumber_DeviceId = 6,
+ GtalkLoginRequest_FieldNumber_LastRmqId = 7,
+ GtalkLoginRequest_FieldNumber_SettingArray = 8,
+ GtalkLoginRequest_FieldNumber_Compress = 9,
+ GtalkLoginRequest_FieldNumber_ReceivedPersistentIdArray = 10,
+ GtalkLoginRequest_FieldNumber_IncludeStreamIds = 11,
+ GtalkLoginRequest_FieldNumber_HeartbeatStat = 13,
+ GtalkLoginRequest_FieldNumber_UseRmq2 = 14,
+ GtalkLoginRequest_FieldNumber_AccountId = 15,
+ GtalkLoginRequest_FieldNumber_AuthService = 16,
+ GtalkLoginRequest_FieldNumber_NetworkType = 17,
+ GtalkLoginRequest_FieldNumber_Status = 18,
+ GtalkLoginRequest_FieldNumber_TokenVersionInfo = 19,
+ GtalkLoginRequest_FieldNumber_CellTower = 20,
+ GtalkLoginRequest_FieldNumber_GcmStartTimeMs = 21,
+ GtalkLoginRequest_FieldNumber_ClientEventArray = 22,
+ GtalkLoginRequest_FieldNumber_OnFallback = 23,
+};
+
+@interface GtalkLoginRequest : GPBMessage
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *id_p;
+/** Test to see if @c id_p has been set. */
+@property(nonatomic, readwrite) BOOL hasId_p;
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *domain;
+/** Test to see if @c domain has been set. */
+@property(nonatomic, readwrite) BOOL hasDomain;
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *user;
+/** Test to see if @c user has been set. */
+@property(nonatomic, readwrite) BOOL hasUser;
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *resource;
+/** Test to see if @c resource has been set. */
+@property(nonatomic, readwrite) BOOL hasResource;
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *authToken;
+/** Test to see if @c authToken has been set. */
+@property(nonatomic, readwrite) BOOL hasAuthToken;
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *deviceId;
+/** Test to see if @c deviceId has been set. */
+@property(nonatomic, readwrite) BOOL hasDeviceId;
+
+
+@property(nonatomic, readwrite) int64_t lastRmqId;
+
+@property(nonatomic, readwrite) BOOL hasLastRmqId;
+
+@property(nonatomic, readwrite, strong, null_resettable) NSMutableArray<GtalkSetting*> *settingArray;
+/** The number of items in @c settingArray without causing the array to be created. */
+@property(nonatomic, readonly) NSUInteger settingArray_Count;
+
+
+@property(nonatomic, readwrite) int32_t compress;
+
+@property(nonatomic, readwrite) BOOL hasCompress;
+
+@property(nonatomic, readwrite, strong, null_resettable) NSMutableArray<NSString*> *receivedPersistentIdArray;
+/** The number of items in @c receivedPersistentIdArray without causing the array to be created. */
+@property(nonatomic, readonly) NSUInteger receivedPersistentIdArray_Count;
+
+
+@property(nonatomic, readwrite) BOOL includeStreamIds;
+
+@property(nonatomic, readwrite) BOOL hasIncludeStreamIds;
+
+@property(nonatomic, readwrite, strong, null_resettable) GtalkHeartbeatStat *heartbeatStat;
+/** Test to see if @c heartbeatStat has been set. */
+@property(nonatomic, readwrite) BOOL hasHeartbeatStat;
+
+
+@property(nonatomic, readwrite) BOOL useRmq2;
+
+@property(nonatomic, readwrite) BOOL hasUseRmq2;
+
+@property(nonatomic, readwrite) int64_t accountId;
+
+@property(nonatomic, readwrite) BOOL hasAccountId;
+
+@property(nonatomic, readwrite) GtalkLoginRequest_AuthService authService;
+
+@property(nonatomic, readwrite) BOOL hasAuthService;
+
+@property(nonatomic, readwrite) int32_t networkType;
+
+@property(nonatomic, readwrite) BOOL hasNetworkType;
+
+@property(nonatomic, readwrite) int64_t status;
+
+@property(nonatomic, readwrite) BOOL hasStatus;
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *tokenVersionInfo;
+/** Test to see if @c tokenVersionInfo has been set. */
+@property(nonatomic, readwrite) BOOL hasTokenVersionInfo;
+
+
+@property(nonatomic, readwrite, strong, null_resettable) GtalkCellTower *cellTower;
+/** Test to see if @c cellTower has been set. */
+@property(nonatomic, readwrite) BOOL hasCellTower;
+
+
+@property(nonatomic, readwrite) uint64_t gcmStartTimeMs;
+
+@property(nonatomic, readwrite) BOOL hasGcmStartTimeMs;
+
+@property(nonatomic, readwrite, strong, null_resettable) NSMutableArray<GtalkClientEvent*> *clientEventArray;
+/** The number of items in @c clientEventArray without causing the array to be created. */
+@property(nonatomic, readonly) NSUInteger clientEventArray_Count;
+
+
+@property(nonatomic, readwrite) BOOL onFallback;
+
+@property(nonatomic, readwrite) BOOL hasOnFallback;
+@end
+
+#pragma mark - GtalkLoginResponse
+
+typedef GPB_ENUM(GtalkLoginResponse_FieldNumber) {
+ GtalkLoginResponse_FieldNumber_Id_p = 1,
+ GtalkLoginResponse_FieldNumber_Jid = 2,
+ GtalkLoginResponse_FieldNumber_Error = 3,
+ GtalkLoginResponse_FieldNumber_SettingArray = 4,
+ GtalkLoginResponse_FieldNumber_StreamId = 5,
+ GtalkLoginResponse_FieldNumber_LastStreamIdReceived = 6,
+ GtalkLoginResponse_FieldNumber_HeartbeatConfig = 7,
+ GtalkLoginResponse_FieldNumber_ServerTimestamp = 8,
+};
+
+@interface GtalkLoginResponse : GPBMessage
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *id_p;
+/** Test to see if @c id_p has been set. */
+@property(nonatomic, readwrite) BOOL hasId_p;
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *jid;
+/** Test to see if @c jid has been set. */
+@property(nonatomic, readwrite) BOOL hasJid;
+
+
+@property(nonatomic, readwrite, strong, null_resettable) GtalkErrorInfo *error;
+/** Test to see if @c error has been set. */
+@property(nonatomic, readwrite) BOOL hasError;
+
+
+@property(nonatomic, readwrite, strong, null_resettable) NSMutableArray<GtalkSetting*> *settingArray;
+/** The number of items in @c settingArray without causing the array to be created. */
+@property(nonatomic, readonly) NSUInteger settingArray_Count;
+
+
+@property(nonatomic, readwrite) int32_t streamId;
+
+@property(nonatomic, readwrite) BOOL hasStreamId;
+
+@property(nonatomic, readwrite) int32_t lastStreamIdReceived;
+
+@property(nonatomic, readwrite) BOOL hasLastStreamIdReceived;
+
+@property(nonatomic, readwrite, strong, null_resettable) GtalkHeartbeatConfig *heartbeatConfig;
+/** Test to see if @c heartbeatConfig has been set. */
+@property(nonatomic, readwrite) BOOL hasHeartbeatConfig;
+
+
+@property(nonatomic, readwrite) int64_t serverTimestamp;
+
+@property(nonatomic, readwrite) BOOL hasServerTimestamp;
+@end
+
+#pragma mark - GtalkBindAccountRequest
+
+typedef GPB_ENUM(GtalkBindAccountRequest_FieldNumber) {
+ GtalkBindAccountRequest_FieldNumber_Id_p = 1,
+ GtalkBindAccountRequest_FieldNumber_Domain = 2,
+ GtalkBindAccountRequest_FieldNumber_User = 3,
+ GtalkBindAccountRequest_FieldNumber_Resource = 4,
+ GtalkBindAccountRequest_FieldNumber_AuthToken = 5,
+ GtalkBindAccountRequest_FieldNumber_PersistentId = 6,
+ GtalkBindAccountRequest_FieldNumber_StreamId = 7,
+ GtalkBindAccountRequest_FieldNumber_LastStreamIdReceived = 8,
+ GtalkBindAccountRequest_FieldNumber_AccountId = 9,
+};
+
+@interface GtalkBindAccountRequest : GPBMessage
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *id_p;
+/** Test to see if @c id_p has been set. */
+@property(nonatomic, readwrite) BOOL hasId_p;
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *domain;
+/** Test to see if @c domain has been set. */
+@property(nonatomic, readwrite) BOOL hasDomain;
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *user;
+/** Test to see if @c user has been set. */
+@property(nonatomic, readwrite) BOOL hasUser;
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *resource;
+/** Test to see if @c resource has been set. */
+@property(nonatomic, readwrite) BOOL hasResource;
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *authToken;
+/** Test to see if @c authToken has been set. */
+@property(nonatomic, readwrite) BOOL hasAuthToken;
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *persistentId;
+/** Test to see if @c persistentId has been set. */
+@property(nonatomic, readwrite) BOOL hasPersistentId;
+
+
+@property(nonatomic, readwrite) int32_t streamId;
+
+@property(nonatomic, readwrite) BOOL hasStreamId;
+
+@property(nonatomic, readwrite) int32_t lastStreamIdReceived;
+
+@property(nonatomic, readwrite) BOOL hasLastStreamIdReceived;
+
+@property(nonatomic, readwrite) int64_t accountId;
+
+@property(nonatomic, readwrite) BOOL hasAccountId;
+@end
+
+#pragma mark - GtalkBindAccountResponse
+
+typedef GPB_ENUM(GtalkBindAccountResponse_FieldNumber) {
+ GtalkBindAccountResponse_FieldNumber_Id_p = 1,
+ GtalkBindAccountResponse_FieldNumber_Jid = 2,
+ GtalkBindAccountResponse_FieldNumber_Error = 3,
+ GtalkBindAccountResponse_FieldNumber_StreamId = 4,
+ GtalkBindAccountResponse_FieldNumber_LastStreamIdReceived = 5,
+};
+
+@interface GtalkBindAccountResponse : GPBMessage
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *id_p;
+/** Test to see if @c id_p has been set. */
+@property(nonatomic, readwrite) BOOL hasId_p;
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *jid;
+/** Test to see if @c jid has been set. */
+@property(nonatomic, readwrite) BOOL hasJid;
+
+
+@property(nonatomic, readwrite, strong, null_resettable) GtalkErrorInfo *error;
+/** Test to see if @c error has been set. */
+@property(nonatomic, readwrite) BOOL hasError;
+
+
+@property(nonatomic, readwrite) int32_t streamId;
+
+@property(nonatomic, readwrite) BOOL hasStreamId;
+
+@property(nonatomic, readwrite) int32_t lastStreamIdReceived;
+
+@property(nonatomic, readwrite) BOOL hasLastStreamIdReceived;
+@end
+
+#pragma mark - GtalkStreamErrorStanza
+
+typedef GPB_ENUM(GtalkStreamErrorStanza_FieldNumber) {
+ GtalkStreamErrorStanza_FieldNumber_Type = 1,
+ GtalkStreamErrorStanza_FieldNumber_Text = 2,
+};
+
+@interface GtalkStreamErrorStanza : GPBMessage
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *type;
+/** Test to see if @c type has been set. */
+@property(nonatomic, readwrite) BOOL hasType;
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *text;
+/** Test to see if @c text has been set. */
+@property(nonatomic, readwrite) BOOL hasText;
+
+@end
+
+#pragma mark - GtalkClose
+
+@interface GtalkClose : GPBMessage
+
+@end
+
+#pragma mark - GtalkExtension
+
+typedef GPB_ENUM(GtalkExtension_FieldNumber) {
+ GtalkExtension_FieldNumber_Id_p = 1,
+ GtalkExtension_FieldNumber_Data_p = 2,
+};
+
+@interface GtalkExtension : GPBMessage
+
+
+@property(nonatomic, readwrite) int32_t id_p;
+
+@property(nonatomic, readwrite) BOOL hasId_p;
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *data_p;
+/** Test to see if @c data_p has been set. */
+@property(nonatomic, readwrite) BOOL hasData_p;
+
+@end
+
+#pragma mark - GtalkMessageStanza
+
+typedef GPB_ENUM(GtalkMessageStanza_FieldNumber) {
+ GtalkMessageStanza_FieldNumber_RmqId = 1,
+ GtalkMessageStanza_FieldNumber_Type = 2,
+ GtalkMessageStanza_FieldNumber_Id_p = 3,
+ GtalkMessageStanza_FieldNumber_From = 4,
+ GtalkMessageStanza_FieldNumber_To = 5,
+ GtalkMessageStanza_FieldNumber_Subject = 6,
+ GtalkMessageStanza_FieldNumber_Body = 7,
+ GtalkMessageStanza_FieldNumber_Thread = 8,
+ GtalkMessageStanza_FieldNumber_Error = 9,
+ GtalkMessageStanza_FieldNumber_ExtensionArray = 10,
+ GtalkMessageStanza_FieldNumber_Nosave = 11,
+ GtalkMessageStanza_FieldNumber_Timestamp = 12,
+ GtalkMessageStanza_FieldNumber_PersistentId = 13,
+ GtalkMessageStanza_FieldNumber_StreamId = 14,
+ GtalkMessageStanza_FieldNumber_LastStreamIdReceived = 15,
+ GtalkMessageStanza_FieldNumber_Read = 16,
+ GtalkMessageStanza_FieldNumber_AccountId = 17,
+};
+
+@interface GtalkMessageStanza : GPBMessage
+
+
+@property(nonatomic, readwrite) int64_t rmqId;
+
+@property(nonatomic, readwrite) BOOL hasRmqId;
+
+@property(nonatomic, readwrite) GtalkMessageStanza_MessageType type;
+
+@property(nonatomic, readwrite) BOOL hasType;
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *id_p;
+/** Test to see if @c id_p has been set. */
+@property(nonatomic, readwrite) BOOL hasId_p;
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *from;
+/** Test to see if @c from has been set. */
+@property(nonatomic, readwrite) BOOL hasFrom;
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *to;
+/** Test to see if @c to has been set. */
+@property(nonatomic, readwrite) BOOL hasTo;
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *subject;
+/** Test to see if @c subject has been set. */
+@property(nonatomic, readwrite) BOOL hasSubject;
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *body;
+/** Test to see if @c body has been set. */
+@property(nonatomic, readwrite) BOOL hasBody;
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *thread;
+/** Test to see if @c thread has been set. */
+@property(nonatomic, readwrite) BOOL hasThread;
+
+
+@property(nonatomic, readwrite, strong, null_resettable) GtalkErrorInfo *error;
+/** Test to see if @c error has been set. */
+@property(nonatomic, readwrite) BOOL hasError;
+
+
+@property(nonatomic, readwrite, strong, null_resettable) NSMutableArray<GtalkExtension*> *extensionArray;
+/** The number of items in @c extensionArray without causing the array to be created. */
+@property(nonatomic, readonly) NSUInteger extensionArray_Count;
+
+
+@property(nonatomic, readwrite) BOOL nosave;
+
+@property(nonatomic, readwrite) BOOL hasNosave;
+
+@property(nonatomic, readwrite) int64_t timestamp;
+
+@property(nonatomic, readwrite) BOOL hasTimestamp;
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *persistentId;
+/** Test to see if @c persistentId has been set. */
+@property(nonatomic, readwrite) BOOL hasPersistentId;
+
+
+@property(nonatomic, readwrite) int32_t streamId;
+
+@property(nonatomic, readwrite) BOOL hasStreamId;
+
+@property(nonatomic, readwrite) int32_t lastStreamIdReceived;
+
+@property(nonatomic, readwrite) BOOL hasLastStreamIdReceived;
+
+@property(nonatomic, readwrite) BOOL read;
+
+@property(nonatomic, readwrite) BOOL hasRead;
+
+@property(nonatomic, readwrite) int64_t accountId;
+
+@property(nonatomic, readwrite) BOOL hasAccountId;
+@end
+
+#pragma mark - GtalkPresenceStanza
+
+typedef GPB_ENUM(GtalkPresenceStanza_FieldNumber) {
+ GtalkPresenceStanza_FieldNumber_RmqId = 1,
+ GtalkPresenceStanza_FieldNumber_Type = 2,
+ GtalkPresenceStanza_FieldNumber_Id_p = 3,
+ GtalkPresenceStanza_FieldNumber_From = 4,
+ GtalkPresenceStanza_FieldNumber_To = 5,
+ GtalkPresenceStanza_FieldNumber_Show = 6,
+ GtalkPresenceStanza_FieldNumber_Status = 7,
+ GtalkPresenceStanza_FieldNumber_Priority = 8,
+ GtalkPresenceStanza_FieldNumber_Error = 9,
+ GtalkPresenceStanza_FieldNumber_ExtensionArray = 10,
+ GtalkPresenceStanza_FieldNumber_Client = 11,
+ GtalkPresenceStanza_FieldNumber_AvatarHash = 12,
+ GtalkPresenceStanza_FieldNumber_PersistentId = 13,
+ GtalkPresenceStanza_FieldNumber_StreamId = 14,
+ GtalkPresenceStanza_FieldNumber_LastStreamIdReceived = 15,
+ GtalkPresenceStanza_FieldNumber_CapabilitiesFlags = 16,
+ GtalkPresenceStanza_FieldNumber_AccountId = 17,
+};
+
+@interface GtalkPresenceStanza : GPBMessage
+
+
+@property(nonatomic, readwrite) int64_t rmqId;
+
+@property(nonatomic, readwrite) BOOL hasRmqId;
+
+@property(nonatomic, readwrite) GtalkPresenceStanza_PresenceType type;
+
+@property(nonatomic, readwrite) BOOL hasType;
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *id_p;
+/** Test to see if @c id_p has been set. */
+@property(nonatomic, readwrite) BOOL hasId_p;
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *from;
+/** Test to see if @c from has been set. */
+@property(nonatomic, readwrite) BOOL hasFrom;
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *to;
+/** Test to see if @c to has been set. */
+@property(nonatomic, readwrite) BOOL hasTo;
+
+
+@property(nonatomic, readwrite) GtalkPresenceStanza_ShowType show;
+
+@property(nonatomic, readwrite) BOOL hasShow;
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *status;
+/** Test to see if @c status has been set. */
+@property(nonatomic, readwrite) BOOL hasStatus;
+
+
+@property(nonatomic, readwrite) int32_t priority;
+
+@property(nonatomic, readwrite) BOOL hasPriority;
+
+@property(nonatomic, readwrite, strong, null_resettable) GtalkErrorInfo *error;
+/** Test to see if @c error has been set. */
+@property(nonatomic, readwrite) BOOL hasError;
+
+
+@property(nonatomic, readwrite, strong, null_resettable) NSMutableArray<GtalkExtension*> *extensionArray;
+/** The number of items in @c extensionArray without causing the array to be created. */
+@property(nonatomic, readonly) NSUInteger extensionArray_Count;
+
+
+@property(nonatomic, readwrite) GtalkPresenceStanza_ClientType client;
+
+@property(nonatomic, readwrite) BOOL hasClient;
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *avatarHash;
+/** Test to see if @c avatarHash has been set. */
+@property(nonatomic, readwrite) BOOL hasAvatarHash;
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *persistentId;
+/** Test to see if @c persistentId has been set. */
+@property(nonatomic, readwrite) BOOL hasPersistentId;
+
+
+@property(nonatomic, readwrite) int32_t streamId;
+
+@property(nonatomic, readwrite) BOOL hasStreamId;
+
+@property(nonatomic, readwrite) int32_t lastStreamIdReceived;
+
+@property(nonatomic, readwrite) BOOL hasLastStreamIdReceived;
+
+@property(nonatomic, readwrite) int32_t capabilitiesFlags;
+
+@property(nonatomic, readwrite) BOOL hasCapabilitiesFlags;
+
+@property(nonatomic, readwrite) int64_t accountId;
+
+@property(nonatomic, readwrite) BOOL hasAccountId;
+@end
+
+#pragma mark - GtalkBatchPresenceStanza
+
+typedef GPB_ENUM(GtalkBatchPresenceStanza_FieldNumber) {
+ GtalkBatchPresenceStanza_FieldNumber_Id_p = 1,
+ GtalkBatchPresenceStanza_FieldNumber_To = 2,
+ GtalkBatchPresenceStanza_FieldNumber_PresenceArray = 3,
+ GtalkBatchPresenceStanza_FieldNumber_PersistentId = 4,
+ GtalkBatchPresenceStanza_FieldNumber_StreamId = 5,
+ GtalkBatchPresenceStanza_FieldNumber_LastStreamIdReceived = 6,
+ GtalkBatchPresenceStanza_FieldNumber_AccountId = 7,
+ GtalkBatchPresenceStanza_FieldNumber_Type = 8,
+ GtalkBatchPresenceStanza_FieldNumber_Error = 9,
+};
+
+@interface GtalkBatchPresenceStanza : GPBMessage
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *id_p;
+/** Test to see if @c id_p has been set. */
+@property(nonatomic, readwrite) BOOL hasId_p;
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *to;
+/** Test to see if @c to has been set. */
+@property(nonatomic, readwrite) BOOL hasTo;
+
+
+@property(nonatomic, readwrite, strong, null_resettable) NSMutableArray<GtalkPresenceStanza*> *presenceArray;
+/** The number of items in @c presenceArray without causing the array to be created. */
+@property(nonatomic, readonly) NSUInteger presenceArray_Count;
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *persistentId;
+/** Test to see if @c persistentId has been set. */
+@property(nonatomic, readwrite) BOOL hasPersistentId;
+
+
+@property(nonatomic, readwrite) int32_t streamId;
+
+@property(nonatomic, readwrite) BOOL hasStreamId;
+
+@property(nonatomic, readwrite) int32_t lastStreamIdReceived;
+
+@property(nonatomic, readwrite) BOOL hasLastStreamIdReceived;
+
+@property(nonatomic, readwrite) int64_t accountId;
+
+@property(nonatomic, readwrite) BOOL hasAccountId;
+
+@property(nonatomic, readwrite) GtalkBatchPresenceStanza_Type type;
+
+@property(nonatomic, readwrite) BOOL hasType;
+
+@property(nonatomic, readwrite, strong, null_resettable) GtalkErrorInfo *error;
+/** Test to see if @c error has been set. */
+@property(nonatomic, readwrite) BOOL hasError;
+
+@end
+
+#pragma mark - GtalkIqStanza
+
+typedef GPB_ENUM(GtalkIqStanza_FieldNumber) {
+ GtalkIqStanza_FieldNumber_RmqId = 1,
+ GtalkIqStanza_FieldNumber_Type = 2,
+ GtalkIqStanza_FieldNumber_Id_p = 3,
+ GtalkIqStanza_FieldNumber_From = 4,
+ GtalkIqStanza_FieldNumber_To = 5,
+ GtalkIqStanza_FieldNumber_Error = 6,
+ GtalkIqStanza_FieldNumber_Extension = 7,
+ GtalkIqStanza_FieldNumber_PersistentId = 8,
+ GtalkIqStanza_FieldNumber_StreamId = 9,
+ GtalkIqStanza_FieldNumber_LastStreamIdReceived = 10,
+ GtalkIqStanza_FieldNumber_AccountId = 11,
+ GtalkIqStanza_FieldNumber_Status = 12,
+};
+
+@interface GtalkIqStanza : GPBMessage
+
+
+@property(nonatomic, readwrite) int64_t rmqId;
+
+@property(nonatomic, readwrite) BOOL hasRmqId;
+
+@property(nonatomic, readwrite) GtalkIqStanza_IqType type;
+
+@property(nonatomic, readwrite) BOOL hasType;
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *id_p;
+/** Test to see if @c id_p has been set. */
+@property(nonatomic, readwrite) BOOL hasId_p;
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *from;
+/** Test to see if @c from has been set. */
+@property(nonatomic, readwrite) BOOL hasFrom;
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *to;
+/** Test to see if @c to has been set. */
+@property(nonatomic, readwrite) BOOL hasTo;
+
+
+@property(nonatomic, readwrite, strong, null_resettable) GtalkErrorInfo *error;
+/** Test to see if @c error has been set. */
+@property(nonatomic, readwrite) BOOL hasError;
+
+
+@property(nonatomic, readwrite, strong, null_resettable) GtalkExtension *extension;
+/** Test to see if @c extension has been set. */
+@property(nonatomic, readwrite) BOOL hasExtension;
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *persistentId;
+/** Test to see if @c persistentId has been set. */
+@property(nonatomic, readwrite) BOOL hasPersistentId;
+
+
+@property(nonatomic, readwrite) int32_t streamId;
+
+@property(nonatomic, readwrite) BOOL hasStreamId;
+
+@property(nonatomic, readwrite) int32_t lastStreamIdReceived;
+
+@property(nonatomic, readwrite) BOOL hasLastStreamIdReceived;
+
+@property(nonatomic, readwrite) int64_t accountId;
+
+@property(nonatomic, readwrite) BOOL hasAccountId;
+
+@property(nonatomic, readwrite) int64_t status;
+
+@property(nonatomic, readwrite) BOOL hasStatus;
+@end
+
+#pragma mark - GtalkAppData
+
+typedef GPB_ENUM(GtalkAppData_FieldNumber) {
+ GtalkAppData_FieldNumber_Key = 1,
+ GtalkAppData_FieldNumber_Value = 2,
+};
+
+@interface GtalkAppData : GPBMessage
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *key;
+/** Test to see if @c key has been set. */
+@property(nonatomic, readwrite) BOOL hasKey;
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *value;
+/** Test to see if @c value has been set. */
+@property(nonatomic, readwrite) BOOL hasValue;
+
+@end
+
+#pragma mark - GtalkDataMessageStanza
+
+typedef GPB_ENUM(GtalkDataMessageStanza_FieldNumber) {
+ GtalkDataMessageStanza_FieldNumber_RmqId = 1,
+ GtalkDataMessageStanza_FieldNumber_Id_p = 2,
+ GtalkDataMessageStanza_FieldNumber_From = 3,
+ GtalkDataMessageStanza_FieldNumber_To = 4,
+ GtalkDataMessageStanza_FieldNumber_Category = 5,
+ GtalkDataMessageStanza_FieldNumber_Token = 6,
+ GtalkDataMessageStanza_FieldNumber_AppDataArray = 7,
+ GtalkDataMessageStanza_FieldNumber_FromTrustedServer = 8,
+ GtalkDataMessageStanza_FieldNumber_PersistentId = 9,
+ GtalkDataMessageStanza_FieldNumber_StreamId = 10,
+ GtalkDataMessageStanza_FieldNumber_LastStreamIdReceived = 11,
+ GtalkDataMessageStanza_FieldNumber_Permission = 12,
+ GtalkDataMessageStanza_FieldNumber_RegId = 13,
+ GtalkDataMessageStanza_FieldNumber_PkgSignature = 14,
+ GtalkDataMessageStanza_FieldNumber_ClientId = 15,
+ GtalkDataMessageStanza_FieldNumber_DeviceUserId = 16,
+ GtalkDataMessageStanza_FieldNumber_Ttl = 17,
+ GtalkDataMessageStanza_FieldNumber_Sent = 18,
+ GtalkDataMessageStanza_FieldNumber_Queued = 19,
+ GtalkDataMessageStanza_FieldNumber_Status = 20,
+ GtalkDataMessageStanza_FieldNumber_RawData = 21,
+ GtalkDataMessageStanza_FieldNumber_MaxDelay = 22,
+ GtalkDataMessageStanza_FieldNumber_ActualDelay = 23,
+ GtalkDataMessageStanza_FieldNumber_ImmediateAck = 24,
+ GtalkDataMessageStanza_FieldNumber_DeliveryReceiptRequested = 25,
+ GtalkDataMessageStanza_FieldNumber_ExternalMessageId = 26,
+ GtalkDataMessageStanza_FieldNumber_Flags = 27,
+ GtalkDataMessageStanza_FieldNumber_CellTower = 28,
+ GtalkDataMessageStanza_FieldNumber_Priority = 29,
+};
+
+@interface GtalkDataMessageStanza : GPBMessage
+
+
+@property(nonatomic, readwrite) int64_t rmqId;
+
+@property(nonatomic, readwrite) BOOL hasRmqId;
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *id_p;
+/** Test to see if @c id_p has been set. */
+@property(nonatomic, readwrite) BOOL hasId_p;
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *from;
+/** Test to see if @c from has been set. */
+@property(nonatomic, readwrite) BOOL hasFrom;
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *to;
+/** Test to see if @c to has been set. */
+@property(nonatomic, readwrite) BOOL hasTo;
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *category;
+/** Test to see if @c category has been set. */
+@property(nonatomic, readwrite) BOOL hasCategory;
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *token;
+/** Test to see if @c token has been set. */
+@property(nonatomic, readwrite) BOOL hasToken;
+
+
+@property(nonatomic, readwrite, strong, null_resettable) NSMutableArray<GtalkAppData*> *appDataArray;
+/** The number of items in @c appDataArray without causing the array to be created. */
+@property(nonatomic, readonly) NSUInteger appDataArray_Count;
+
+
+@property(nonatomic, readwrite) BOOL fromTrustedServer;
+
+@property(nonatomic, readwrite) BOOL hasFromTrustedServer;
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *persistentId;
+/** Test to see if @c persistentId has been set. */
+@property(nonatomic, readwrite) BOOL hasPersistentId;
+
+
+@property(nonatomic, readwrite) int32_t streamId;
+
+@property(nonatomic, readwrite) BOOL hasStreamId;
+
+@property(nonatomic, readwrite) int32_t lastStreamIdReceived;
+
+@property(nonatomic, readwrite) BOOL hasLastStreamIdReceived;
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *permission;
+/** Test to see if @c permission has been set. */
+@property(nonatomic, readwrite) BOOL hasPermission;
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *regId;
+/** Test to see if @c regId has been set. */
+@property(nonatomic, readwrite) BOOL hasRegId;
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *pkgSignature;
+/** Test to see if @c pkgSignature has been set. */
+@property(nonatomic, readwrite) BOOL hasPkgSignature;
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *clientId;
+/** Test to see if @c clientId has been set. */
+@property(nonatomic, readwrite) BOOL hasClientId;
+
+
+@property(nonatomic, readwrite) int64_t deviceUserId;
+
+@property(nonatomic, readwrite) BOOL hasDeviceUserId;
+
+@property(nonatomic, readwrite) int32_t ttl;
+
+@property(nonatomic, readwrite) BOOL hasTtl;
+
+@property(nonatomic, readwrite) int64_t sent;
+
+@property(nonatomic, readwrite) BOOL hasSent;
+
+@property(nonatomic, readwrite) int32_t queued;
+
+@property(nonatomic, readwrite) BOOL hasQueued;
+
+@property(nonatomic, readwrite) int64_t status;
+
+@property(nonatomic, readwrite) BOOL hasStatus;
+
+@property(nonatomic, readwrite, copy, null_resettable) NSData *rawData;
+/** Test to see if @c rawData has been set. */
+@property(nonatomic, readwrite) BOOL hasRawData;
+
+
+@property(nonatomic, readwrite) int32_t maxDelay;
+
+@property(nonatomic, readwrite) BOOL hasMaxDelay;
+
+@property(nonatomic, readwrite) int32_t actualDelay;
+
+@property(nonatomic, readwrite) BOOL hasActualDelay;
+
+@property(nonatomic, readwrite) BOOL immediateAck;
+
+@property(nonatomic, readwrite) BOOL hasImmediateAck;
+
+@property(nonatomic, readwrite) BOOL deliveryReceiptRequested;
+
+@property(nonatomic, readwrite) BOOL hasDeliveryReceiptRequested;
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *externalMessageId;
+/** Test to see if @c externalMessageId has been set. */
+@property(nonatomic, readwrite) BOOL hasExternalMessageId;
+
+
+@property(nonatomic, readwrite) int64_t flags;
+
+@property(nonatomic, readwrite) BOOL hasFlags;
+
+@property(nonatomic, readwrite, strong, null_resettable) GtalkCellTower *cellTower;
+/** Test to see if @c cellTower has been set. */
+@property(nonatomic, readwrite) BOOL hasCellTower;
+
+
+@property(nonatomic, readwrite) int32_t priority;
+
+@property(nonatomic, readwrite) BOOL hasPriority;
+@end
+
+#pragma mark - GtalkTalkMetadata
+
+typedef GPB_ENUM(GtalkTalkMetadata_FieldNumber) {
+ GtalkTalkMetadata_FieldNumber_Foreground = 1,
+};
+
+@interface GtalkTalkMetadata : GPBMessage
+
+
+@property(nonatomic, readwrite) BOOL foreground;
+
+@property(nonatomic, readwrite) BOOL hasForeground;
+@end
+
+#pragma mark - GtalkCellTower
+
+typedef GPB_ENUM(GtalkCellTower_FieldNumber) {
+ GtalkCellTower_FieldNumber_Id_p = 1,
+ GtalkCellTower_FieldNumber_KnownCongestionStatus = 2,
+};
+
+@interface GtalkCellTower : GPBMessage
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *id_p;
+/** Test to see if @c id_p has been set. */
+@property(nonatomic, readwrite) BOOL hasId_p;
+
+
+@property(nonatomic, readwrite) int32_t knownCongestionStatus;
+
+@property(nonatomic, readwrite) BOOL hasKnownCongestionStatus;
+@end
+
+#pragma mark - GtalkClientEvent
+
+typedef GPB_ENUM(GtalkClientEvent_FieldNumber) {
+ GtalkClientEvent_FieldNumber_Type = 1,
+ GtalkClientEvent_FieldNumber_NumberDiscardedEvents = 100,
+ GtalkClientEvent_FieldNumber_NetworkType = 200,
+ GtalkClientEvent_FieldNumber_NetworkPort = 201,
+ GtalkClientEvent_FieldNumber_TimeConnectionStartedMs = 202,
+ GtalkClientEvent_FieldNumber_TimeConnectionEndedMs = 203,
+ GtalkClientEvent_FieldNumber_ErrorCode = 204,
+ GtalkClientEvent_FieldNumber_TimeConnectionEstablishedMs = 300,
+};
+
+@interface GtalkClientEvent : GPBMessage
+
+
+@property(nonatomic, readwrite) GtalkClientEvent_Type type;
+
+@property(nonatomic, readwrite) BOOL hasType;
+
+@property(nonatomic, readwrite) uint32_t numberDiscardedEvents;
+
+@property(nonatomic, readwrite) BOOL hasNumberDiscardedEvents;
+
+@property(nonatomic, readwrite) int32_t networkType;
+
+@property(nonatomic, readwrite) BOOL hasNetworkType;
+
+@property(nonatomic, readwrite) int32_t networkPort;
+
+@property(nonatomic, readwrite) BOOL hasNetworkPort;
+
+@property(nonatomic, readwrite) uint64_t timeConnectionStartedMs;
+
+@property(nonatomic, readwrite) BOOL hasTimeConnectionStartedMs;
+
+@property(nonatomic, readwrite) uint64_t timeConnectionEndedMs;
+
+@property(nonatomic, readwrite) BOOL hasTimeConnectionEndedMs;
+
+@property(nonatomic, readwrite) int32_t errorCode;
+
+@property(nonatomic, readwrite) BOOL hasErrorCode;
+
+@property(nonatomic, readwrite) uint64_t timeConnectionEstablishedMs;
+
+@property(nonatomic, readwrite) BOOL hasTimeConnectionEstablishedMs;
+@end
+
+NS_ASSUME_NONNULL_END
+
+CF_EXTERN_C_END
+
+#pragma clang diagnostic pop
+
+// @@protoc_insertion_point(global_scope)
diff --git a/Firebase/Messaging/Protos/GtalkCore.pbobjc.m b/Firebase/Messaging/Protos/GtalkCore.pbobjc.m
new file mode 100644
index 0000000..f4efe22
--- /dev/null
+++ b/Firebase/Messaging/Protos/GtalkCore.pbobjc.m
@@ -0,0 +1,2947 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Generated by the protocol buffer compiler. DO NOT EDIT!
+// source: buzz/mobile/proto/gtalk_core.proto
+
+// This CPP symbol can be defined to use imports that match up to the framework
+// imports needed when using CocoaPods.
+#if !defined(GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS)
+ #define GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS 0
+#endif
+
+#if GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS
+ #import <Protobuf/GPBProtocolBuffers_RuntimeSupport.h>
+#else
+ #import "GPBProtocolBuffers_RuntimeSupport.h"
+#endif
+
+ #import "GtalkCore.pbobjc.h"
+// @@protoc_insertion_point(imports)
+
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdeprecated-declarations"
+
+#pragma mark - GtalkGtalkCoreRoot
+
+@implementation GtalkGtalkCoreRoot
+
+// No extensions in the file and no imports, so no need to generate
+// +extensionRegistry.
+
+@end
+
+#pragma mark - GtalkGtalkCoreRoot_FileDescriptor
+
+static GPBFileDescriptor *GtalkGtalkCoreRoot_FileDescriptor(void) {
+ // This is called by +initialize so there is no need to worry
+ // about thread safety of the singleton.
+ static GPBFileDescriptor *descriptor = NULL;
+ if (!descriptor) {
+ GPB_DEBUG_CHECK_RUNTIME_VERSIONS();
+ descriptor = [[GPBFileDescriptor alloc] initWithPackage:@"mobilegtalk"
+ objcPrefix:@"Gtalk"
+ syntax:GPBFileSyntaxProto2];
+ }
+ return descriptor;
+}
+
+#pragma mark - GtalkHeartbeatPing
+
+@implementation GtalkHeartbeatPing
+
+@dynamic hasStreamId, streamId;
+@dynamic hasLastStreamIdReceived, lastStreamIdReceived;
+@dynamic hasStatus, status;
+@dynamic hasCellTower, cellTower;
+@dynamic hasIntervalMs, intervalMs;
+
+typedef struct GtalkHeartbeatPing__storage_ {
+ uint32_t _has_storage_[1];
+ int32_t streamId;
+ int32_t lastStreamIdReceived;
+ int32_t intervalMs;
+ GtalkCellTower *cellTower;
+ int64_t status;
+} GtalkHeartbeatPing__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "streamId",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkHeartbeatPing_FieldNumber_StreamId,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(GtalkHeartbeatPing__storage_, streamId),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt32,
+ },
+ {
+ .name = "lastStreamIdReceived",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkHeartbeatPing_FieldNumber_LastStreamIdReceived,
+ .hasIndex = 1,
+ .offset = (uint32_t)offsetof(GtalkHeartbeatPing__storage_, lastStreamIdReceived),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt32,
+ },
+ {
+ .name = "status",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkHeartbeatPing_FieldNumber_Status,
+ .hasIndex = 2,
+ .offset = (uint32_t)offsetof(GtalkHeartbeatPing__storage_, status),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt64,
+ },
+ {
+ .name = "cellTower",
+ .dataTypeSpecific.className = GPBStringifySymbol(GtalkCellTower),
+ .number = GtalkHeartbeatPing_FieldNumber_CellTower,
+ .hasIndex = 3,
+ .offset = (uint32_t)offsetof(GtalkHeartbeatPing__storage_, cellTower),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeMessage,
+ },
+ {
+ .name = "intervalMs",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkHeartbeatPing_FieldNumber_IntervalMs,
+ .hasIndex = 4,
+ .offset = (uint32_t)offsetof(GtalkHeartbeatPing__storage_, intervalMs),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt32,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GtalkHeartbeatPing class]
+ rootClass:[GtalkGtalkCoreRoot class]
+ file:GtalkGtalkCoreRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GtalkHeartbeatPing__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+#pragma mark - GtalkHeartbeatAck
+
+@implementation GtalkHeartbeatAck
+
+@dynamic hasStreamId, streamId;
+@dynamic hasLastStreamIdReceived, lastStreamIdReceived;
+@dynamic hasStatus, status;
+@dynamic hasCellTower, cellTower;
+@dynamic hasIntervalMs, intervalMs;
+
+typedef struct GtalkHeartbeatAck__storage_ {
+ uint32_t _has_storage_[1];
+ int32_t streamId;
+ int32_t lastStreamIdReceived;
+ int32_t intervalMs;
+ GtalkCellTower *cellTower;
+ int64_t status;
+} GtalkHeartbeatAck__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "streamId",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkHeartbeatAck_FieldNumber_StreamId,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(GtalkHeartbeatAck__storage_, streamId),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt32,
+ },
+ {
+ .name = "lastStreamIdReceived",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkHeartbeatAck_FieldNumber_LastStreamIdReceived,
+ .hasIndex = 1,
+ .offset = (uint32_t)offsetof(GtalkHeartbeatAck__storage_, lastStreamIdReceived),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt32,
+ },
+ {
+ .name = "status",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkHeartbeatAck_FieldNumber_Status,
+ .hasIndex = 2,
+ .offset = (uint32_t)offsetof(GtalkHeartbeatAck__storage_, status),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt64,
+ },
+ {
+ .name = "cellTower",
+ .dataTypeSpecific.className = GPBStringifySymbol(GtalkCellTower),
+ .number = GtalkHeartbeatAck_FieldNumber_CellTower,
+ .hasIndex = 3,
+ .offset = (uint32_t)offsetof(GtalkHeartbeatAck__storage_, cellTower),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeMessage,
+ },
+ {
+ .name = "intervalMs",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkHeartbeatAck_FieldNumber_IntervalMs,
+ .hasIndex = 4,
+ .offset = (uint32_t)offsetof(GtalkHeartbeatAck__storage_, intervalMs),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt32,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GtalkHeartbeatAck class]
+ rootClass:[GtalkGtalkCoreRoot class]
+ file:GtalkGtalkCoreRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GtalkHeartbeatAck__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+#pragma mark - GtalkErrorInfo
+
+@implementation GtalkErrorInfo
+
+@dynamic hasCode, code;
+@dynamic hasMessage, message;
+@dynamic hasType, type;
+@dynamic hasExtension, extension;
+
+typedef struct GtalkErrorInfo__storage_ {
+ uint32_t _has_storage_[1];
+ int32_t code;
+ NSString *message;
+ NSString *type;
+ GtalkExtension *extension;
+} GtalkErrorInfo__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "code",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkErrorInfo_FieldNumber_Code,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(GtalkErrorInfo__storage_, code),
+ .flags = GPBFieldRequired,
+ .dataType = GPBDataTypeInt32,
+ },
+ {
+ .name = "message",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkErrorInfo_FieldNumber_Message,
+ .hasIndex = 1,
+ .offset = (uint32_t)offsetof(GtalkErrorInfo__storage_, message),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "type",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkErrorInfo_FieldNumber_Type,
+ .hasIndex = 2,
+ .offset = (uint32_t)offsetof(GtalkErrorInfo__storage_, type),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "extension",
+ .dataTypeSpecific.className = GPBStringifySymbol(GtalkExtension),
+ .number = GtalkErrorInfo_FieldNumber_Extension,
+ .hasIndex = 3,
+ .offset = (uint32_t)offsetof(GtalkErrorInfo__storage_, extension),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeMessage,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GtalkErrorInfo class]
+ rootClass:[GtalkGtalkCoreRoot class]
+ file:GtalkGtalkCoreRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GtalkErrorInfo__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+#pragma mark - GtalkSetting
+
+@implementation GtalkSetting
+
+@dynamic hasName, name;
+@dynamic hasValue, value;
+
+typedef struct GtalkSetting__storage_ {
+ uint32_t _has_storage_[1];
+ NSString *name;
+ NSString *value;
+} GtalkSetting__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "name",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkSetting_FieldNumber_Name,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(GtalkSetting__storage_, name),
+ .flags = GPBFieldRequired,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "value",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkSetting_FieldNumber_Value,
+ .hasIndex = 1,
+ .offset = (uint32_t)offsetof(GtalkSetting__storage_, value),
+ .flags = GPBFieldRequired,
+ .dataType = GPBDataTypeString,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GtalkSetting class]
+ rootClass:[GtalkGtalkCoreRoot class]
+ file:GtalkGtalkCoreRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GtalkSetting__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+#pragma mark - GtalkHeartbeatStat
+
+@implementation GtalkHeartbeatStat
+
+@dynamic hasIp, ip;
+@dynamic hasTimeout, timeout;
+@dynamic hasIntervalMs, intervalMs;
+
+typedef struct GtalkHeartbeatStat__storage_ {
+ uint32_t _has_storage_[1];
+ int32_t intervalMs;
+ NSString *ip;
+} GtalkHeartbeatStat__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "ip",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkHeartbeatStat_FieldNumber_Ip,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(GtalkHeartbeatStat__storage_, ip),
+ .flags = GPBFieldRequired,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "timeout",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkHeartbeatStat_FieldNumber_Timeout,
+ .hasIndex = 1,
+ .offset = 2, // Stored in _has_storage_ to save space.
+ .flags = GPBFieldRequired,
+ .dataType = GPBDataTypeBool,
+ },
+ {
+ .name = "intervalMs",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkHeartbeatStat_FieldNumber_IntervalMs,
+ .hasIndex = 3,
+ .offset = (uint32_t)offsetof(GtalkHeartbeatStat__storage_, intervalMs),
+ .flags = GPBFieldRequired,
+ .dataType = GPBDataTypeInt32,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GtalkHeartbeatStat class]
+ rootClass:[GtalkGtalkCoreRoot class]
+ file:GtalkGtalkCoreRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GtalkHeartbeatStat__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+#pragma mark - GtalkHeartbeatConfig
+
+@implementation GtalkHeartbeatConfig
+
+@dynamic hasUploadStat, uploadStat;
+@dynamic hasIp, ip;
+@dynamic hasIntervalMs, intervalMs;
+
+typedef struct GtalkHeartbeatConfig__storage_ {
+ uint32_t _has_storage_[1];
+ int32_t intervalMs;
+ NSString *ip;
+} GtalkHeartbeatConfig__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "uploadStat",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkHeartbeatConfig_FieldNumber_UploadStat,
+ .hasIndex = 0,
+ .offset = 1, // Stored in _has_storage_ to save space.
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeBool,
+ },
+ {
+ .name = "ip",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkHeartbeatConfig_FieldNumber_Ip,
+ .hasIndex = 2,
+ .offset = (uint32_t)offsetof(GtalkHeartbeatConfig__storage_, ip),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "intervalMs",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkHeartbeatConfig_FieldNumber_IntervalMs,
+ .hasIndex = 3,
+ .offset = (uint32_t)offsetof(GtalkHeartbeatConfig__storage_, intervalMs),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt32,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GtalkHeartbeatConfig class]
+ rootClass:[GtalkGtalkCoreRoot class]
+ file:GtalkGtalkCoreRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GtalkHeartbeatConfig__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+#pragma mark - GtalkLoginRequest
+
+@implementation GtalkLoginRequest
+
+@dynamic hasId_p, id_p;
+@dynamic hasDomain, domain;
+@dynamic hasUser, user;
+@dynamic hasResource, resource;
+@dynamic hasAuthToken, authToken;
+@dynamic hasDeviceId, deviceId;
+@dynamic hasLastRmqId, lastRmqId;
+@dynamic settingArray, settingArray_Count;
+@dynamic hasCompress, compress;
+@dynamic receivedPersistentIdArray, receivedPersistentIdArray_Count;
+@dynamic hasIncludeStreamIds, includeStreamIds;
+@dynamic hasHeartbeatStat, heartbeatStat;
+@dynamic hasUseRmq2, useRmq2;
+@dynamic hasAccountId, accountId;
+@dynamic hasAuthService, authService;
+@dynamic hasNetworkType, networkType;
+@dynamic hasStatus, status;
+@dynamic hasTokenVersionInfo, tokenVersionInfo;
+@dynamic hasCellTower, cellTower;
+@dynamic hasGcmStartTimeMs, gcmStartTimeMs;
+@dynamic clientEventArray, clientEventArray_Count;
+@dynamic hasOnFallback, onFallback;
+
+typedef struct GtalkLoginRequest__storage_ {
+ uint32_t _has_storage_[1];
+ int32_t compress;
+ GtalkLoginRequest_AuthService authService;
+ int32_t networkType;
+ NSString *id_p;
+ NSString *domain;
+ NSString *user;
+ NSString *resource;
+ NSString *authToken;
+ NSString *deviceId;
+ NSMutableArray *settingArray;
+ NSMutableArray *receivedPersistentIdArray;
+ GtalkHeartbeatStat *heartbeatStat;
+ NSString *tokenVersionInfo;
+ GtalkCellTower *cellTower;
+ NSMutableArray *clientEventArray;
+ int64_t lastRmqId;
+ int64_t accountId;
+ int64_t status;
+ uint64_t gcmStartTimeMs;
+} GtalkLoginRequest__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "id_p",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkLoginRequest_FieldNumber_Id_p,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(GtalkLoginRequest__storage_, id_p),
+ .flags = GPBFieldRequired,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "domain",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkLoginRequest_FieldNumber_Domain,
+ .hasIndex = 1,
+ .offset = (uint32_t)offsetof(GtalkLoginRequest__storage_, domain),
+ .flags = GPBFieldRequired,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "user",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkLoginRequest_FieldNumber_User,
+ .hasIndex = 2,
+ .offset = (uint32_t)offsetof(GtalkLoginRequest__storage_, user),
+ .flags = GPBFieldRequired,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "resource",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkLoginRequest_FieldNumber_Resource,
+ .hasIndex = 3,
+ .offset = (uint32_t)offsetof(GtalkLoginRequest__storage_, resource),
+ .flags = GPBFieldRequired,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "authToken",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkLoginRequest_FieldNumber_AuthToken,
+ .hasIndex = 4,
+ .offset = (uint32_t)offsetof(GtalkLoginRequest__storage_, authToken),
+ .flags = GPBFieldRequired,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "deviceId",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkLoginRequest_FieldNumber_DeviceId,
+ .hasIndex = 5,
+ .offset = (uint32_t)offsetof(GtalkLoginRequest__storage_, deviceId),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "lastRmqId",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkLoginRequest_FieldNumber_LastRmqId,
+ .hasIndex = 6,
+ .offset = (uint32_t)offsetof(GtalkLoginRequest__storage_, lastRmqId),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt64,
+ },
+ {
+ .name = "settingArray",
+ .dataTypeSpecific.className = GPBStringifySymbol(GtalkSetting),
+ .number = GtalkLoginRequest_FieldNumber_SettingArray,
+ .hasIndex = GPBNoHasBit,
+ .offset = (uint32_t)offsetof(GtalkLoginRequest__storage_, settingArray),
+ .flags = GPBFieldRepeated,
+ .dataType = GPBDataTypeMessage,
+ },
+ {
+ .name = "compress",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkLoginRequest_FieldNumber_Compress,
+ .hasIndex = 7,
+ .offset = (uint32_t)offsetof(GtalkLoginRequest__storage_, compress),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt32,
+ },
+ {
+ .name = "receivedPersistentIdArray",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkLoginRequest_FieldNumber_ReceivedPersistentIdArray,
+ .hasIndex = GPBNoHasBit,
+ .offset = (uint32_t)offsetof(GtalkLoginRequest__storage_, receivedPersistentIdArray),
+ .flags = GPBFieldRepeated,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "includeStreamIds",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkLoginRequest_FieldNumber_IncludeStreamIds,
+ .hasIndex = 8,
+ .offset = 9, // Stored in _has_storage_ to save space.
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeBool,
+ },
+ {
+ .name = "heartbeatStat",
+ .dataTypeSpecific.className = GPBStringifySymbol(GtalkHeartbeatStat),
+ .number = GtalkLoginRequest_FieldNumber_HeartbeatStat,
+ .hasIndex = 10,
+ .offset = (uint32_t)offsetof(GtalkLoginRequest__storage_, heartbeatStat),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeMessage,
+ },
+ {
+ .name = "useRmq2",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkLoginRequest_FieldNumber_UseRmq2,
+ .hasIndex = 11,
+ .offset = 12, // Stored in _has_storage_ to save space.
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeBool,
+ },
+ {
+ .name = "accountId",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkLoginRequest_FieldNumber_AccountId,
+ .hasIndex = 13,
+ .offset = (uint32_t)offsetof(GtalkLoginRequest__storage_, accountId),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt64,
+ },
+ {
+ .name = "authService",
+ .dataTypeSpecific.enumDescFunc = GtalkLoginRequest_AuthService_EnumDescriptor,
+ .number = GtalkLoginRequest_FieldNumber_AuthService,
+ .hasIndex = 14,
+ .offset = (uint32_t)offsetof(GtalkLoginRequest__storage_, authService),
+ .flags = (GPBFieldFlags)(GPBFieldOptional | GPBFieldHasEnumDescriptor),
+ .dataType = GPBDataTypeEnum,
+ },
+ {
+ .name = "networkType",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkLoginRequest_FieldNumber_NetworkType,
+ .hasIndex = 15,
+ .offset = (uint32_t)offsetof(GtalkLoginRequest__storage_, networkType),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt32,
+ },
+ {
+ .name = "status",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkLoginRequest_FieldNumber_Status,
+ .hasIndex = 16,
+ .offset = (uint32_t)offsetof(GtalkLoginRequest__storage_, status),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt64,
+ },
+ {
+ .name = "tokenVersionInfo",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkLoginRequest_FieldNumber_TokenVersionInfo,
+ .hasIndex = 17,
+ .offset = (uint32_t)offsetof(GtalkLoginRequest__storage_, tokenVersionInfo),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "cellTower",
+ .dataTypeSpecific.className = GPBStringifySymbol(GtalkCellTower),
+ .number = GtalkLoginRequest_FieldNumber_CellTower,
+ .hasIndex = 18,
+ .offset = (uint32_t)offsetof(GtalkLoginRequest__storage_, cellTower),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeMessage,
+ },
+ {
+ .name = "gcmStartTimeMs",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkLoginRequest_FieldNumber_GcmStartTimeMs,
+ .hasIndex = 19,
+ .offset = (uint32_t)offsetof(GtalkLoginRequest__storage_, gcmStartTimeMs),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeUInt64,
+ },
+ {
+ .name = "clientEventArray",
+ .dataTypeSpecific.className = GPBStringifySymbol(GtalkClientEvent),
+ .number = GtalkLoginRequest_FieldNumber_ClientEventArray,
+ .hasIndex = GPBNoHasBit,
+ .offset = (uint32_t)offsetof(GtalkLoginRequest__storage_, clientEventArray),
+ .flags = GPBFieldRepeated,
+ .dataType = GPBDataTypeMessage,
+ },
+ {
+ .name = "onFallback",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkLoginRequest_FieldNumber_OnFallback,
+ .hasIndex = 20,
+ .offset = 21, // Stored in _has_storage_ to save space.
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeBool,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GtalkLoginRequest class]
+ rootClass:[GtalkGtalkCoreRoot class]
+ file:GtalkGtalkCoreRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GtalkLoginRequest__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+#pragma mark - Enum GtalkLoginRequest_AuthService
+
+GPBEnumDescriptor *GtalkLoginRequest_AuthService_EnumDescriptor(void) {
+ static GPBEnumDescriptor *descriptor = NULL;
+ if (!descriptor) {
+ static const char *valueNames =
+ "Mail\000AndroidCloudToDeviceMessage\000Android"
+ "Id\000";
+ static const int32_t values[] = {
+ GtalkLoginRequest_AuthService_Mail,
+ GtalkLoginRequest_AuthService_AndroidCloudToDeviceMessage,
+ GtalkLoginRequest_AuthService_AndroidId,
+ };
+ GPBEnumDescriptor *worker =
+ [GPBEnumDescriptor allocDescriptorForName:GPBNSStringifySymbol(GtalkLoginRequest_AuthService)
+ valueNames:valueNames
+ values:values
+ count:(uint32_t)(sizeof(values) / sizeof(int32_t))
+ enumVerifier:GtalkLoginRequest_AuthService_IsValidValue];
+ if (!OSAtomicCompareAndSwapPtrBarrier(nil, worker, (void * volatile *)&descriptor)) {
+ [worker release];
+ }
+ }
+ return descriptor;
+}
+
+BOOL GtalkLoginRequest_AuthService_IsValidValue(int32_t value__) {
+ switch (value__) {
+ case GtalkLoginRequest_AuthService_Mail:
+ case GtalkLoginRequest_AuthService_AndroidCloudToDeviceMessage:
+ case GtalkLoginRequest_AuthService_AndroidId:
+ return YES;
+ default:
+ return NO;
+ }
+}
+
+#pragma mark - GtalkLoginResponse
+
+@implementation GtalkLoginResponse
+
+@dynamic hasId_p, id_p;
+@dynamic hasJid, jid;
+@dynamic hasError, error;
+@dynamic settingArray, settingArray_Count;
+@dynamic hasStreamId, streamId;
+@dynamic hasLastStreamIdReceived, lastStreamIdReceived;
+@dynamic hasHeartbeatConfig, heartbeatConfig;
+@dynamic hasServerTimestamp, serverTimestamp;
+
+typedef struct GtalkLoginResponse__storage_ {
+ uint32_t _has_storage_[1];
+ int32_t streamId;
+ int32_t lastStreamIdReceived;
+ NSString *id_p;
+ NSString *jid;
+ GtalkErrorInfo *error;
+ NSMutableArray *settingArray;
+ GtalkHeartbeatConfig *heartbeatConfig;
+ int64_t serverTimestamp;
+} GtalkLoginResponse__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "id_p",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkLoginResponse_FieldNumber_Id_p,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(GtalkLoginResponse__storage_, id_p),
+ .flags = GPBFieldRequired,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "jid",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkLoginResponse_FieldNumber_Jid,
+ .hasIndex = 1,
+ .offset = (uint32_t)offsetof(GtalkLoginResponse__storage_, jid),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "error",
+ .dataTypeSpecific.className = GPBStringifySymbol(GtalkErrorInfo),
+ .number = GtalkLoginResponse_FieldNumber_Error,
+ .hasIndex = 2,
+ .offset = (uint32_t)offsetof(GtalkLoginResponse__storage_, error),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeMessage,
+ },
+ {
+ .name = "settingArray",
+ .dataTypeSpecific.className = GPBStringifySymbol(GtalkSetting),
+ .number = GtalkLoginResponse_FieldNumber_SettingArray,
+ .hasIndex = GPBNoHasBit,
+ .offset = (uint32_t)offsetof(GtalkLoginResponse__storage_, settingArray),
+ .flags = GPBFieldRepeated,
+ .dataType = GPBDataTypeMessage,
+ },
+ {
+ .name = "streamId",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkLoginResponse_FieldNumber_StreamId,
+ .hasIndex = 3,
+ .offset = (uint32_t)offsetof(GtalkLoginResponse__storage_, streamId),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt32,
+ },
+ {
+ .name = "lastStreamIdReceived",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkLoginResponse_FieldNumber_LastStreamIdReceived,
+ .hasIndex = 4,
+ .offset = (uint32_t)offsetof(GtalkLoginResponse__storage_, lastStreamIdReceived),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt32,
+ },
+ {
+ .name = "heartbeatConfig",
+ .dataTypeSpecific.className = GPBStringifySymbol(GtalkHeartbeatConfig),
+ .number = GtalkLoginResponse_FieldNumber_HeartbeatConfig,
+ .hasIndex = 5,
+ .offset = (uint32_t)offsetof(GtalkLoginResponse__storage_, heartbeatConfig),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeMessage,
+ },
+ {
+ .name = "serverTimestamp",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkLoginResponse_FieldNumber_ServerTimestamp,
+ .hasIndex = 6,
+ .offset = (uint32_t)offsetof(GtalkLoginResponse__storage_, serverTimestamp),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt64,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GtalkLoginResponse class]
+ rootClass:[GtalkGtalkCoreRoot class]
+ file:GtalkGtalkCoreRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GtalkLoginResponse__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+#pragma mark - GtalkBindAccountRequest
+
+@implementation GtalkBindAccountRequest
+
+@dynamic hasId_p, id_p;
+@dynamic hasDomain, domain;
+@dynamic hasUser, user;
+@dynamic hasResource, resource;
+@dynamic hasAuthToken, authToken;
+@dynamic hasPersistentId, persistentId;
+@dynamic hasStreamId, streamId;
+@dynamic hasLastStreamIdReceived, lastStreamIdReceived;
+@dynamic hasAccountId, accountId;
+
+typedef struct GtalkBindAccountRequest__storage_ {
+ uint32_t _has_storage_[1];
+ int32_t streamId;
+ int32_t lastStreamIdReceived;
+ NSString *id_p;
+ NSString *domain;
+ NSString *user;
+ NSString *resource;
+ NSString *authToken;
+ NSString *persistentId;
+ int64_t accountId;
+} GtalkBindAccountRequest__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "id_p",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkBindAccountRequest_FieldNumber_Id_p,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(GtalkBindAccountRequest__storage_, id_p),
+ .flags = GPBFieldRequired,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "domain",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkBindAccountRequest_FieldNumber_Domain,
+ .hasIndex = 1,
+ .offset = (uint32_t)offsetof(GtalkBindAccountRequest__storage_, domain),
+ .flags = GPBFieldRequired,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "user",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkBindAccountRequest_FieldNumber_User,
+ .hasIndex = 2,
+ .offset = (uint32_t)offsetof(GtalkBindAccountRequest__storage_, user),
+ .flags = GPBFieldRequired,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "resource",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkBindAccountRequest_FieldNumber_Resource,
+ .hasIndex = 3,
+ .offset = (uint32_t)offsetof(GtalkBindAccountRequest__storage_, resource),
+ .flags = GPBFieldRequired,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "authToken",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkBindAccountRequest_FieldNumber_AuthToken,
+ .hasIndex = 4,
+ .offset = (uint32_t)offsetof(GtalkBindAccountRequest__storage_, authToken),
+ .flags = GPBFieldRequired,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "persistentId",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkBindAccountRequest_FieldNumber_PersistentId,
+ .hasIndex = 5,
+ .offset = (uint32_t)offsetof(GtalkBindAccountRequest__storage_, persistentId),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "streamId",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkBindAccountRequest_FieldNumber_StreamId,
+ .hasIndex = 6,
+ .offset = (uint32_t)offsetof(GtalkBindAccountRequest__storage_, streamId),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt32,
+ },
+ {
+ .name = "lastStreamIdReceived",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkBindAccountRequest_FieldNumber_LastStreamIdReceived,
+ .hasIndex = 7,
+ .offset = (uint32_t)offsetof(GtalkBindAccountRequest__storage_, lastStreamIdReceived),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt32,
+ },
+ {
+ .name = "accountId",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkBindAccountRequest_FieldNumber_AccountId,
+ .hasIndex = 8,
+ .offset = (uint32_t)offsetof(GtalkBindAccountRequest__storage_, accountId),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt64,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GtalkBindAccountRequest class]
+ rootClass:[GtalkGtalkCoreRoot class]
+ file:GtalkGtalkCoreRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GtalkBindAccountRequest__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+#pragma mark - GtalkBindAccountResponse
+
+@implementation GtalkBindAccountResponse
+
+@dynamic hasId_p, id_p;
+@dynamic hasJid, jid;
+@dynamic hasError, error;
+@dynamic hasStreamId, streamId;
+@dynamic hasLastStreamIdReceived, lastStreamIdReceived;
+
+typedef struct GtalkBindAccountResponse__storage_ {
+ uint32_t _has_storage_[1];
+ int32_t streamId;
+ int32_t lastStreamIdReceived;
+ NSString *id_p;
+ NSString *jid;
+ GtalkErrorInfo *error;
+} GtalkBindAccountResponse__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "id_p",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkBindAccountResponse_FieldNumber_Id_p,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(GtalkBindAccountResponse__storage_, id_p),
+ .flags = GPBFieldRequired,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "jid",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkBindAccountResponse_FieldNumber_Jid,
+ .hasIndex = 1,
+ .offset = (uint32_t)offsetof(GtalkBindAccountResponse__storage_, jid),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "error",
+ .dataTypeSpecific.className = GPBStringifySymbol(GtalkErrorInfo),
+ .number = GtalkBindAccountResponse_FieldNumber_Error,
+ .hasIndex = 2,
+ .offset = (uint32_t)offsetof(GtalkBindAccountResponse__storage_, error),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeMessage,
+ },
+ {
+ .name = "streamId",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkBindAccountResponse_FieldNumber_StreamId,
+ .hasIndex = 3,
+ .offset = (uint32_t)offsetof(GtalkBindAccountResponse__storage_, streamId),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt32,
+ },
+ {
+ .name = "lastStreamIdReceived",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkBindAccountResponse_FieldNumber_LastStreamIdReceived,
+ .hasIndex = 4,
+ .offset = (uint32_t)offsetof(GtalkBindAccountResponse__storage_, lastStreamIdReceived),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt32,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GtalkBindAccountResponse class]
+ rootClass:[GtalkGtalkCoreRoot class]
+ file:GtalkGtalkCoreRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GtalkBindAccountResponse__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+#pragma mark - GtalkStreamErrorStanza
+
+@implementation GtalkStreamErrorStanza
+
+@dynamic hasType, type;
+@dynamic hasText, text;
+
+typedef struct GtalkStreamErrorStanza__storage_ {
+ uint32_t _has_storage_[1];
+ NSString *type;
+ NSString *text;
+} GtalkStreamErrorStanza__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "type",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkStreamErrorStanza_FieldNumber_Type,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(GtalkStreamErrorStanza__storage_, type),
+ .flags = GPBFieldRequired,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "text",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkStreamErrorStanza_FieldNumber_Text,
+ .hasIndex = 1,
+ .offset = (uint32_t)offsetof(GtalkStreamErrorStanza__storage_, text),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GtalkStreamErrorStanza class]
+ rootClass:[GtalkGtalkCoreRoot class]
+ file:GtalkGtalkCoreRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GtalkStreamErrorStanza__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+#pragma mark - GtalkClose
+
+@implementation GtalkClose
+
+
+typedef struct GtalkClose__storage_ {
+ uint32_t _has_storage_[1];
+} GtalkClose__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GtalkClose class]
+ rootClass:[GtalkGtalkCoreRoot class]
+ file:GtalkGtalkCoreRoot_FileDescriptor()
+ fields:NULL
+ fieldCount:0
+ storageSize:sizeof(GtalkClose__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+#pragma mark - GtalkExtension
+
+@implementation GtalkExtension
+
+@dynamic hasId_p, id_p;
+@dynamic hasData_p, data_p;
+
+typedef struct GtalkExtension__storage_ {
+ uint32_t _has_storage_[1];
+ int32_t id_p;
+ NSString *data_p;
+} GtalkExtension__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "id_p",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkExtension_FieldNumber_Id_p,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(GtalkExtension__storage_, id_p),
+ .flags = GPBFieldRequired,
+ .dataType = GPBDataTypeInt32,
+ },
+ {
+ .name = "data_p",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkExtension_FieldNumber_Data_p,
+ .hasIndex = 1,
+ .offset = (uint32_t)offsetof(GtalkExtension__storage_, data_p),
+ .flags = GPBFieldRequired,
+ .dataType = GPBDataTypeString,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GtalkExtension class]
+ rootClass:[GtalkGtalkCoreRoot class]
+ file:GtalkGtalkCoreRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GtalkExtension__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+#pragma mark - GtalkMessageStanza
+
+@implementation GtalkMessageStanza
+
+@dynamic hasRmqId, rmqId;
+@dynamic hasType, type;
+@dynamic hasId_p, id_p;
+@dynamic hasFrom, from;
+@dynamic hasTo, to;
+@dynamic hasSubject, subject;
+@dynamic hasBody, body;
+@dynamic hasThread, thread;
+@dynamic hasError, error;
+@dynamic extensionArray, extensionArray_Count;
+@dynamic hasNosave, nosave;
+@dynamic hasTimestamp, timestamp;
+@dynamic hasPersistentId, persistentId;
+@dynamic hasStreamId, streamId;
+@dynamic hasLastStreamIdReceived, lastStreamIdReceived;
+@dynamic hasRead, read;
+@dynamic hasAccountId, accountId;
+
+typedef struct GtalkMessageStanza__storage_ {
+ uint32_t _has_storage_[1];
+ GtalkMessageStanza_MessageType type;
+ int32_t streamId;
+ int32_t lastStreamIdReceived;
+ NSString *id_p;
+ NSString *from;
+ NSString *to;
+ NSString *subject;
+ NSString *body;
+ NSString *thread;
+ GtalkErrorInfo *error;
+ NSMutableArray *extensionArray;
+ NSString *persistentId;
+ int64_t rmqId;
+ int64_t timestamp;
+ int64_t accountId;
+} GtalkMessageStanza__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "rmqId",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkMessageStanza_FieldNumber_RmqId,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(GtalkMessageStanza__storage_, rmqId),
+ .flags = (GPBFieldFlags)(GPBFieldOptional | GPBFieldTextFormatNameCustom),
+ .dataType = GPBDataTypeInt64,
+ },
+ {
+ .name = "type",
+ .dataTypeSpecific.enumDescFunc = GtalkMessageStanza_MessageType_EnumDescriptor,
+ .number = GtalkMessageStanza_FieldNumber_Type,
+ .hasIndex = 1,
+ .offset = (uint32_t)offsetof(GtalkMessageStanza__storage_, type),
+ .flags = (GPBFieldFlags)(GPBFieldOptional | GPBFieldHasEnumDescriptor),
+ .dataType = GPBDataTypeEnum,
+ },
+ {
+ .name = "id_p",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkMessageStanza_FieldNumber_Id_p,
+ .hasIndex = 2,
+ .offset = (uint32_t)offsetof(GtalkMessageStanza__storage_, id_p),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "from",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkMessageStanza_FieldNumber_From,
+ .hasIndex = 3,
+ .offset = (uint32_t)offsetof(GtalkMessageStanza__storage_, from),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "to",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkMessageStanza_FieldNumber_To,
+ .hasIndex = 4,
+ .offset = (uint32_t)offsetof(GtalkMessageStanza__storage_, to),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "subject",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkMessageStanza_FieldNumber_Subject,
+ .hasIndex = 5,
+ .offset = (uint32_t)offsetof(GtalkMessageStanza__storage_, subject),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "body",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkMessageStanza_FieldNumber_Body,
+ .hasIndex = 6,
+ .offset = (uint32_t)offsetof(GtalkMessageStanza__storage_, body),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "thread",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkMessageStanza_FieldNumber_Thread,
+ .hasIndex = 7,
+ .offset = (uint32_t)offsetof(GtalkMessageStanza__storage_, thread),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "error",
+ .dataTypeSpecific.className = GPBStringifySymbol(GtalkErrorInfo),
+ .number = GtalkMessageStanza_FieldNumber_Error,
+ .hasIndex = 8,
+ .offset = (uint32_t)offsetof(GtalkMessageStanza__storage_, error),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeMessage,
+ },
+ {
+ .name = "extensionArray",
+ .dataTypeSpecific.className = GPBStringifySymbol(GtalkExtension),
+ .number = GtalkMessageStanza_FieldNumber_ExtensionArray,
+ .hasIndex = GPBNoHasBit,
+ .offset = (uint32_t)offsetof(GtalkMessageStanza__storage_, extensionArray),
+ .flags = GPBFieldRepeated,
+ .dataType = GPBDataTypeMessage,
+ },
+ {
+ .name = "nosave",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkMessageStanza_FieldNumber_Nosave,
+ .hasIndex = 9,
+ .offset = 10, // Stored in _has_storage_ to save space.
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeBool,
+ },
+ {
+ .name = "timestamp",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkMessageStanza_FieldNumber_Timestamp,
+ .hasIndex = 11,
+ .offset = (uint32_t)offsetof(GtalkMessageStanza__storage_, timestamp),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt64,
+ },
+ {
+ .name = "persistentId",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkMessageStanza_FieldNumber_PersistentId,
+ .hasIndex = 12,
+ .offset = (uint32_t)offsetof(GtalkMessageStanza__storage_, persistentId),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "streamId",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkMessageStanza_FieldNumber_StreamId,
+ .hasIndex = 13,
+ .offset = (uint32_t)offsetof(GtalkMessageStanza__storage_, streamId),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt32,
+ },
+ {
+ .name = "lastStreamIdReceived",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkMessageStanza_FieldNumber_LastStreamIdReceived,
+ .hasIndex = 14,
+ .offset = (uint32_t)offsetof(GtalkMessageStanza__storage_, lastStreamIdReceived),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt32,
+ },
+ {
+ .name = "read",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkMessageStanza_FieldNumber_Read,
+ .hasIndex = 15,
+ .offset = 16, // Stored in _has_storage_ to save space.
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeBool,
+ },
+ {
+ .name = "accountId",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkMessageStanza_FieldNumber_AccountId,
+ .hasIndex = 17,
+ .offset = (uint32_t)offsetof(GtalkMessageStanza__storage_, accountId),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt64,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GtalkMessageStanza class]
+ rootClass:[GtalkGtalkCoreRoot class]
+ file:GtalkGtalkCoreRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GtalkMessageStanza__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+#if !GPBOBJC_SKIP_MESSAGE_TEXTFORMAT_EXTRAS
+ static const char *extraTextFormatInfo =
+ "\001\001\005\000";
+ [localDescriptor setupExtraTextInfo:extraTextFormatInfo];
+#endif // !GPBOBJC_SKIP_MESSAGE_TEXTFORMAT_EXTRAS
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+#pragma mark - Enum GtalkMessageStanza_MessageType
+
+GPBEnumDescriptor *GtalkMessageStanza_MessageType_EnumDescriptor(void) {
+ static GPBEnumDescriptor *descriptor = NULL;
+ if (!descriptor) {
+ static const char *valueNames =
+ "Normal\000Chat\000Groupchat\000Headline\000Error\000";
+ static const int32_t values[] = {
+ GtalkMessageStanza_MessageType_Normal,
+ GtalkMessageStanza_MessageType_Chat,
+ GtalkMessageStanza_MessageType_Groupchat,
+ GtalkMessageStanza_MessageType_Headline,
+ GtalkMessageStanza_MessageType_Error,
+ };
+ GPBEnumDescriptor *worker =
+ [GPBEnumDescriptor allocDescriptorForName:GPBNSStringifySymbol(GtalkMessageStanza_MessageType)
+ valueNames:valueNames
+ values:values
+ count:(uint32_t)(sizeof(values) / sizeof(int32_t))
+ enumVerifier:GtalkMessageStanza_MessageType_IsValidValue];
+ if (!OSAtomicCompareAndSwapPtrBarrier(nil, worker, (void * volatile *)&descriptor)) {
+ [worker release];
+ }
+ }
+ return descriptor;
+}
+
+BOOL GtalkMessageStanza_MessageType_IsValidValue(int32_t value__) {
+ switch (value__) {
+ case GtalkMessageStanza_MessageType_Normal:
+ case GtalkMessageStanza_MessageType_Chat:
+ case GtalkMessageStanza_MessageType_Groupchat:
+ case GtalkMessageStanza_MessageType_Headline:
+ case GtalkMessageStanza_MessageType_Error:
+ return YES;
+ default:
+ return NO;
+ }
+}
+
+#pragma mark - GtalkPresenceStanza
+
+@implementation GtalkPresenceStanza
+
+@dynamic hasRmqId, rmqId;
+@dynamic hasType, type;
+@dynamic hasId_p, id_p;
+@dynamic hasFrom, from;
+@dynamic hasTo, to;
+@dynamic hasShow, show;
+@dynamic hasStatus, status;
+@dynamic hasPriority, priority;
+@dynamic hasError, error;
+@dynamic extensionArray, extensionArray_Count;
+@dynamic hasClient, client;
+@dynamic hasAvatarHash, avatarHash;
+@dynamic hasPersistentId, persistentId;
+@dynamic hasStreamId, streamId;
+@dynamic hasLastStreamIdReceived, lastStreamIdReceived;
+@dynamic hasCapabilitiesFlags, capabilitiesFlags;
+@dynamic hasAccountId, accountId;
+
+typedef struct GtalkPresenceStanza__storage_ {
+ uint32_t _has_storage_[1];
+ GtalkPresenceStanza_PresenceType type;
+ GtalkPresenceStanza_ShowType show;
+ int32_t priority;
+ GtalkPresenceStanza_ClientType client;
+ int32_t streamId;
+ int32_t lastStreamIdReceived;
+ int32_t capabilitiesFlags;
+ NSString *id_p;
+ NSString *from;
+ NSString *to;
+ NSString *status;
+ GtalkErrorInfo *error;
+ NSMutableArray *extensionArray;
+ NSString *avatarHash;
+ NSString *persistentId;
+ int64_t rmqId;
+ int64_t accountId;
+} GtalkPresenceStanza__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "rmqId",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkPresenceStanza_FieldNumber_RmqId,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(GtalkPresenceStanza__storage_, rmqId),
+ .flags = (GPBFieldFlags)(GPBFieldOptional | GPBFieldTextFormatNameCustom),
+ .dataType = GPBDataTypeInt64,
+ },
+ {
+ .name = "type",
+ .dataTypeSpecific.enumDescFunc = GtalkPresenceStanza_PresenceType_EnumDescriptor,
+ .number = GtalkPresenceStanza_FieldNumber_Type,
+ .hasIndex = 1,
+ .offset = (uint32_t)offsetof(GtalkPresenceStanza__storage_, type),
+ .flags = (GPBFieldFlags)(GPBFieldOptional | GPBFieldHasEnumDescriptor),
+ .dataType = GPBDataTypeEnum,
+ },
+ {
+ .name = "id_p",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkPresenceStanza_FieldNumber_Id_p,
+ .hasIndex = 2,
+ .offset = (uint32_t)offsetof(GtalkPresenceStanza__storage_, id_p),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "from",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkPresenceStanza_FieldNumber_From,
+ .hasIndex = 3,
+ .offset = (uint32_t)offsetof(GtalkPresenceStanza__storage_, from),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "to",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkPresenceStanza_FieldNumber_To,
+ .hasIndex = 4,
+ .offset = (uint32_t)offsetof(GtalkPresenceStanza__storage_, to),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "show",
+ .dataTypeSpecific.enumDescFunc = GtalkPresenceStanza_ShowType_EnumDescriptor,
+ .number = GtalkPresenceStanza_FieldNumber_Show,
+ .hasIndex = 5,
+ .offset = (uint32_t)offsetof(GtalkPresenceStanza__storage_, show),
+ .flags = (GPBFieldFlags)(GPBFieldOptional | GPBFieldHasEnumDescriptor),
+ .dataType = GPBDataTypeEnum,
+ },
+ {
+ .name = "status",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkPresenceStanza_FieldNumber_Status,
+ .hasIndex = 6,
+ .offset = (uint32_t)offsetof(GtalkPresenceStanza__storage_, status),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "priority",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkPresenceStanza_FieldNumber_Priority,
+ .hasIndex = 7,
+ .offset = (uint32_t)offsetof(GtalkPresenceStanza__storage_, priority),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt32,
+ },
+ {
+ .name = "error",
+ .dataTypeSpecific.className = GPBStringifySymbol(GtalkErrorInfo),
+ .number = GtalkPresenceStanza_FieldNumber_Error,
+ .hasIndex = 8,
+ .offset = (uint32_t)offsetof(GtalkPresenceStanza__storage_, error),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeMessage,
+ },
+ {
+ .name = "extensionArray",
+ .dataTypeSpecific.className = GPBStringifySymbol(GtalkExtension),
+ .number = GtalkPresenceStanza_FieldNumber_ExtensionArray,
+ .hasIndex = GPBNoHasBit,
+ .offset = (uint32_t)offsetof(GtalkPresenceStanza__storage_, extensionArray),
+ .flags = GPBFieldRepeated,
+ .dataType = GPBDataTypeMessage,
+ },
+ {
+ .name = "client",
+ .dataTypeSpecific.enumDescFunc = GtalkPresenceStanza_ClientType_EnumDescriptor,
+ .number = GtalkPresenceStanza_FieldNumber_Client,
+ .hasIndex = 9,
+ .offset = (uint32_t)offsetof(GtalkPresenceStanza__storage_, client),
+ .flags = (GPBFieldFlags)(GPBFieldOptional | GPBFieldHasEnumDescriptor),
+ .dataType = GPBDataTypeEnum,
+ },
+ {
+ .name = "avatarHash",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkPresenceStanza_FieldNumber_AvatarHash,
+ .hasIndex = 10,
+ .offset = (uint32_t)offsetof(GtalkPresenceStanza__storage_, avatarHash),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "persistentId",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkPresenceStanza_FieldNumber_PersistentId,
+ .hasIndex = 11,
+ .offset = (uint32_t)offsetof(GtalkPresenceStanza__storage_, persistentId),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "streamId",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkPresenceStanza_FieldNumber_StreamId,
+ .hasIndex = 12,
+ .offset = (uint32_t)offsetof(GtalkPresenceStanza__storage_, streamId),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt32,
+ },
+ {
+ .name = "lastStreamIdReceived",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkPresenceStanza_FieldNumber_LastStreamIdReceived,
+ .hasIndex = 13,
+ .offset = (uint32_t)offsetof(GtalkPresenceStanza__storage_, lastStreamIdReceived),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt32,
+ },
+ {
+ .name = "capabilitiesFlags",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkPresenceStanza_FieldNumber_CapabilitiesFlags,
+ .hasIndex = 14,
+ .offset = (uint32_t)offsetof(GtalkPresenceStanza__storage_, capabilitiesFlags),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt32,
+ },
+ {
+ .name = "accountId",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkPresenceStanza_FieldNumber_AccountId,
+ .hasIndex = 15,
+ .offset = (uint32_t)offsetof(GtalkPresenceStanza__storage_, accountId),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt64,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GtalkPresenceStanza class]
+ rootClass:[GtalkGtalkCoreRoot class]
+ file:GtalkGtalkCoreRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GtalkPresenceStanza__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+#if !GPBOBJC_SKIP_MESSAGE_TEXTFORMAT_EXTRAS
+ static const char *extraTextFormatInfo =
+ "\001\001\005\000";
+ [localDescriptor setupExtraTextInfo:extraTextFormatInfo];
+#endif // !GPBOBJC_SKIP_MESSAGE_TEXTFORMAT_EXTRAS
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+#pragma mark - Enum GtalkPresenceStanza_PresenceType
+
+GPBEnumDescriptor *GtalkPresenceStanza_PresenceType_EnumDescriptor(void) {
+ static GPBEnumDescriptor *descriptor = NULL;
+ if (!descriptor) {
+ static const char *valueNames =
+ "Unavailable\000Subscribe\000Subscribed\000Unsubsc"
+ "ribe\000Unsubscribed\000Probe\000Error\000";
+ static const int32_t values[] = {
+ GtalkPresenceStanza_PresenceType_Unavailable,
+ GtalkPresenceStanza_PresenceType_Subscribe,
+ GtalkPresenceStanza_PresenceType_Subscribed,
+ GtalkPresenceStanza_PresenceType_Unsubscribe,
+ GtalkPresenceStanza_PresenceType_Unsubscribed,
+ GtalkPresenceStanza_PresenceType_Probe,
+ GtalkPresenceStanza_PresenceType_Error,
+ };
+ GPBEnumDescriptor *worker =
+ [GPBEnumDescriptor allocDescriptorForName:GPBNSStringifySymbol(GtalkPresenceStanza_PresenceType)
+ valueNames:valueNames
+ values:values
+ count:(uint32_t)(sizeof(values) / sizeof(int32_t))
+ enumVerifier:GtalkPresenceStanza_PresenceType_IsValidValue];
+ if (!OSAtomicCompareAndSwapPtrBarrier(nil, worker, (void * volatile *)&descriptor)) {
+ [worker release];
+ }
+ }
+ return descriptor;
+}
+
+BOOL GtalkPresenceStanza_PresenceType_IsValidValue(int32_t value__) {
+ switch (value__) {
+ case GtalkPresenceStanza_PresenceType_Unavailable:
+ case GtalkPresenceStanza_PresenceType_Subscribe:
+ case GtalkPresenceStanza_PresenceType_Subscribed:
+ case GtalkPresenceStanza_PresenceType_Unsubscribe:
+ case GtalkPresenceStanza_PresenceType_Unsubscribed:
+ case GtalkPresenceStanza_PresenceType_Probe:
+ case GtalkPresenceStanza_PresenceType_Error:
+ return YES;
+ default:
+ return NO;
+ }
+}
+
+#pragma mark - Enum GtalkPresenceStanza_ShowType
+
+GPBEnumDescriptor *GtalkPresenceStanza_ShowType_EnumDescriptor(void) {
+ static GPBEnumDescriptor *descriptor = NULL;
+ if (!descriptor) {
+ static const char *valueNames =
+ "Away\000Chat\000Dnd\000Xa\000";
+ static const int32_t values[] = {
+ GtalkPresenceStanza_ShowType_Away,
+ GtalkPresenceStanza_ShowType_Chat,
+ GtalkPresenceStanza_ShowType_Dnd,
+ GtalkPresenceStanza_ShowType_Xa,
+ };
+ GPBEnumDescriptor *worker =
+ [GPBEnumDescriptor allocDescriptorForName:GPBNSStringifySymbol(GtalkPresenceStanza_ShowType)
+ valueNames:valueNames
+ values:values
+ count:(uint32_t)(sizeof(values) / sizeof(int32_t))
+ enumVerifier:GtalkPresenceStanza_ShowType_IsValidValue];
+ if (!OSAtomicCompareAndSwapPtrBarrier(nil, worker, (void * volatile *)&descriptor)) {
+ [worker release];
+ }
+ }
+ return descriptor;
+}
+
+BOOL GtalkPresenceStanza_ShowType_IsValidValue(int32_t value__) {
+ switch (value__) {
+ case GtalkPresenceStanza_ShowType_Away:
+ case GtalkPresenceStanza_ShowType_Chat:
+ case GtalkPresenceStanza_ShowType_Dnd:
+ case GtalkPresenceStanza_ShowType_Xa:
+ return YES;
+ default:
+ return NO;
+ }
+}
+
+#pragma mark - Enum GtalkPresenceStanza_ClientType
+
+GPBEnumDescriptor *GtalkPresenceStanza_ClientType_EnumDescriptor(void) {
+ static GPBEnumDescriptor *descriptor = NULL;
+ if (!descriptor) {
+ static const char *valueNames =
+ "Mobile\000Android\000";
+ static const int32_t values[] = {
+ GtalkPresenceStanza_ClientType_Mobile,
+ GtalkPresenceStanza_ClientType_Android,
+ };
+ GPBEnumDescriptor *worker =
+ [GPBEnumDescriptor allocDescriptorForName:GPBNSStringifySymbol(GtalkPresenceStanza_ClientType)
+ valueNames:valueNames
+ values:values
+ count:(uint32_t)(sizeof(values) / sizeof(int32_t))
+ enumVerifier:GtalkPresenceStanza_ClientType_IsValidValue];
+ if (!OSAtomicCompareAndSwapPtrBarrier(nil, worker, (void * volatile *)&descriptor)) {
+ [worker release];
+ }
+ }
+ return descriptor;
+}
+
+BOOL GtalkPresenceStanza_ClientType_IsValidValue(int32_t value__) {
+ switch (value__) {
+ case GtalkPresenceStanza_ClientType_Mobile:
+ case GtalkPresenceStanza_ClientType_Android:
+ return YES;
+ default:
+ return NO;
+ }
+}
+
+#pragma mark - Enum GtalkPresenceStanza_CapabilitiesFlags
+
+GPBEnumDescriptor *GtalkPresenceStanza_CapabilitiesFlags_EnumDescriptor(void) {
+ static GPBEnumDescriptor *descriptor = NULL;
+ if (!descriptor) {
+ static const char *valueNames =
+ "HasVoiceV1\000HasVideoV1\000HasCameraV1\000HasPmu"
+ "cV1\000";
+ static const int32_t values[] = {
+ GtalkPresenceStanza_CapabilitiesFlags_HasVoiceV1,
+ GtalkPresenceStanza_CapabilitiesFlags_HasVideoV1,
+ GtalkPresenceStanza_CapabilitiesFlags_HasCameraV1,
+ GtalkPresenceStanza_CapabilitiesFlags_HasPmucV1,
+ };
+ GPBEnumDescriptor *worker =
+ [GPBEnumDescriptor allocDescriptorForName:GPBNSStringifySymbol(GtalkPresenceStanza_CapabilitiesFlags)
+ valueNames:valueNames
+ values:values
+ count:(uint32_t)(sizeof(values) / sizeof(int32_t))
+ enumVerifier:GtalkPresenceStanza_CapabilitiesFlags_IsValidValue];
+ if (!OSAtomicCompareAndSwapPtrBarrier(nil, worker, (void * volatile *)&descriptor)) {
+ [worker release];
+ }
+ }
+ return descriptor;
+}
+
+BOOL GtalkPresenceStanza_CapabilitiesFlags_IsValidValue(int32_t value__) {
+ switch (value__) {
+ case GtalkPresenceStanza_CapabilitiesFlags_HasVoiceV1:
+ case GtalkPresenceStanza_CapabilitiesFlags_HasVideoV1:
+ case GtalkPresenceStanza_CapabilitiesFlags_HasCameraV1:
+ case GtalkPresenceStanza_CapabilitiesFlags_HasPmucV1:
+ return YES;
+ default:
+ return NO;
+ }
+}
+
+#pragma mark - GtalkBatchPresenceStanza
+
+@implementation GtalkBatchPresenceStanza
+
+@dynamic hasId_p, id_p;
+@dynamic hasTo, to;
+@dynamic presenceArray, presenceArray_Count;
+@dynamic hasPersistentId, persistentId;
+@dynamic hasStreamId, streamId;
+@dynamic hasLastStreamIdReceived, lastStreamIdReceived;
+@dynamic hasAccountId, accountId;
+@dynamic hasType, type;
+@dynamic hasError, error;
+
+typedef struct GtalkBatchPresenceStanza__storage_ {
+ uint32_t _has_storage_[1];
+ int32_t streamId;
+ int32_t lastStreamIdReceived;
+ GtalkBatchPresenceStanza_Type type;
+ NSString *id_p;
+ NSString *to;
+ NSMutableArray *presenceArray;
+ NSString *persistentId;
+ GtalkErrorInfo *error;
+ int64_t accountId;
+} GtalkBatchPresenceStanza__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "id_p",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkBatchPresenceStanza_FieldNumber_Id_p,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(GtalkBatchPresenceStanza__storage_, id_p),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "to",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkBatchPresenceStanza_FieldNumber_To,
+ .hasIndex = 1,
+ .offset = (uint32_t)offsetof(GtalkBatchPresenceStanza__storage_, to),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "presenceArray",
+ .dataTypeSpecific.className = GPBStringifySymbol(GtalkPresenceStanza),
+ .number = GtalkBatchPresenceStanza_FieldNumber_PresenceArray,
+ .hasIndex = GPBNoHasBit,
+ .offset = (uint32_t)offsetof(GtalkBatchPresenceStanza__storage_, presenceArray),
+ .flags = GPBFieldRepeated,
+ .dataType = GPBDataTypeMessage,
+ },
+ {
+ .name = "persistentId",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkBatchPresenceStanza_FieldNumber_PersistentId,
+ .hasIndex = 2,
+ .offset = (uint32_t)offsetof(GtalkBatchPresenceStanza__storage_, persistentId),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "streamId",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkBatchPresenceStanza_FieldNumber_StreamId,
+ .hasIndex = 3,
+ .offset = (uint32_t)offsetof(GtalkBatchPresenceStanza__storage_, streamId),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt32,
+ },
+ {
+ .name = "lastStreamIdReceived",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkBatchPresenceStanza_FieldNumber_LastStreamIdReceived,
+ .hasIndex = 4,
+ .offset = (uint32_t)offsetof(GtalkBatchPresenceStanza__storage_, lastStreamIdReceived),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt32,
+ },
+ {
+ .name = "accountId",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkBatchPresenceStanza_FieldNumber_AccountId,
+ .hasIndex = 5,
+ .offset = (uint32_t)offsetof(GtalkBatchPresenceStanza__storage_, accountId),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt64,
+ },
+ {
+ .name = "type",
+ .dataTypeSpecific.enumDescFunc = GtalkBatchPresenceStanza_Type_EnumDescriptor,
+ .number = GtalkBatchPresenceStanza_FieldNumber_Type,
+ .hasIndex = 6,
+ .offset = (uint32_t)offsetof(GtalkBatchPresenceStanza__storage_, type),
+ .flags = (GPBFieldFlags)(GPBFieldOptional | GPBFieldHasEnumDescriptor),
+ .dataType = GPBDataTypeEnum,
+ },
+ {
+ .name = "error",
+ .dataTypeSpecific.className = GPBStringifySymbol(GtalkErrorInfo),
+ .number = GtalkBatchPresenceStanza_FieldNumber_Error,
+ .hasIndex = 7,
+ .offset = (uint32_t)offsetof(GtalkBatchPresenceStanza__storage_, error),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeMessage,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GtalkBatchPresenceStanza class]
+ rootClass:[GtalkGtalkCoreRoot class]
+ file:GtalkGtalkCoreRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GtalkBatchPresenceStanza__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+#pragma mark - Enum GtalkBatchPresenceStanza_Type
+
+GPBEnumDescriptor *GtalkBatchPresenceStanza_Type_EnumDescriptor(void) {
+ static GPBEnumDescriptor *descriptor = NULL;
+ if (!descriptor) {
+ static const char *valueNames =
+ "Get\000Set\000";
+ static const int32_t values[] = {
+ GtalkBatchPresenceStanza_Type_Get,
+ GtalkBatchPresenceStanza_Type_Set,
+ };
+ GPBEnumDescriptor *worker =
+ [GPBEnumDescriptor allocDescriptorForName:GPBNSStringifySymbol(GtalkBatchPresenceStanza_Type)
+ valueNames:valueNames
+ values:values
+ count:(uint32_t)(sizeof(values) / sizeof(int32_t))
+ enumVerifier:GtalkBatchPresenceStanza_Type_IsValidValue];
+ if (!OSAtomicCompareAndSwapPtrBarrier(nil, worker, (void * volatile *)&descriptor)) {
+ [worker release];
+ }
+ }
+ return descriptor;
+}
+
+BOOL GtalkBatchPresenceStanza_Type_IsValidValue(int32_t value__) {
+ switch (value__) {
+ case GtalkBatchPresenceStanza_Type_Get:
+ case GtalkBatchPresenceStanza_Type_Set:
+ return YES;
+ default:
+ return NO;
+ }
+}
+
+#pragma mark - GtalkIqStanza
+
+@implementation GtalkIqStanza
+
+@dynamic hasRmqId, rmqId;
+@dynamic hasType, type;
+@dynamic hasId_p, id_p;
+@dynamic hasFrom, from;
+@dynamic hasTo, to;
+@dynamic hasError, error;
+@dynamic hasExtension, extension;
+@dynamic hasPersistentId, persistentId;
+@dynamic hasStreamId, streamId;
+@dynamic hasLastStreamIdReceived, lastStreamIdReceived;
+@dynamic hasAccountId, accountId;
+@dynamic hasStatus, status;
+
+typedef struct GtalkIqStanza__storage_ {
+ uint32_t _has_storage_[1];
+ GtalkIqStanza_IqType type;
+ int32_t streamId;
+ int32_t lastStreamIdReceived;
+ NSString *id_p;
+ NSString *from;
+ NSString *to;
+ GtalkErrorInfo *error;
+ GtalkExtension *extension;
+ NSString *persistentId;
+ int64_t rmqId;
+ int64_t accountId;
+ int64_t status;
+} GtalkIqStanza__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "rmqId",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkIqStanza_FieldNumber_RmqId,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(GtalkIqStanza__storage_, rmqId),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt64,
+ },
+ {
+ .name = "type",
+ .dataTypeSpecific.enumDescFunc = GtalkIqStanza_IqType_EnumDescriptor,
+ .number = GtalkIqStanza_FieldNumber_Type,
+ .hasIndex = 1,
+ .offset = (uint32_t)offsetof(GtalkIqStanza__storage_, type),
+ .flags = (GPBFieldFlags)(GPBFieldRequired | GPBFieldHasEnumDescriptor),
+ .dataType = GPBDataTypeEnum,
+ },
+ {
+ .name = "id_p",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkIqStanza_FieldNumber_Id_p,
+ .hasIndex = 2,
+ .offset = (uint32_t)offsetof(GtalkIqStanza__storage_, id_p),
+ .flags = GPBFieldRequired,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "from",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkIqStanza_FieldNumber_From,
+ .hasIndex = 3,
+ .offset = (uint32_t)offsetof(GtalkIqStanza__storage_, from),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "to",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkIqStanza_FieldNumber_To,
+ .hasIndex = 4,
+ .offset = (uint32_t)offsetof(GtalkIqStanza__storage_, to),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "error",
+ .dataTypeSpecific.className = GPBStringifySymbol(GtalkErrorInfo),
+ .number = GtalkIqStanza_FieldNumber_Error,
+ .hasIndex = 5,
+ .offset = (uint32_t)offsetof(GtalkIqStanza__storage_, error),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeMessage,
+ },
+ {
+ .name = "extension",
+ .dataTypeSpecific.className = GPBStringifySymbol(GtalkExtension),
+ .number = GtalkIqStanza_FieldNumber_Extension,
+ .hasIndex = 6,
+ .offset = (uint32_t)offsetof(GtalkIqStanza__storage_, extension),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeMessage,
+ },
+ {
+ .name = "persistentId",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkIqStanza_FieldNumber_PersistentId,
+ .hasIndex = 7,
+ .offset = (uint32_t)offsetof(GtalkIqStanza__storage_, persistentId),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "streamId",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkIqStanza_FieldNumber_StreamId,
+ .hasIndex = 8,
+ .offset = (uint32_t)offsetof(GtalkIqStanza__storage_, streamId),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt32,
+ },
+ {
+ .name = "lastStreamIdReceived",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkIqStanza_FieldNumber_LastStreamIdReceived,
+ .hasIndex = 9,
+ .offset = (uint32_t)offsetof(GtalkIqStanza__storage_, lastStreamIdReceived),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt32,
+ },
+ {
+ .name = "accountId",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkIqStanza_FieldNumber_AccountId,
+ .hasIndex = 10,
+ .offset = (uint32_t)offsetof(GtalkIqStanza__storage_, accountId),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt64,
+ },
+ {
+ .name = "status",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkIqStanza_FieldNumber_Status,
+ .hasIndex = 11,
+ .offset = (uint32_t)offsetof(GtalkIqStanza__storage_, status),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt64,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GtalkIqStanza class]
+ rootClass:[GtalkGtalkCoreRoot class]
+ file:GtalkGtalkCoreRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GtalkIqStanza__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+#pragma mark - Enum GtalkIqStanza_IqType
+
+GPBEnumDescriptor *GtalkIqStanza_IqType_EnumDescriptor(void) {
+ static GPBEnumDescriptor *descriptor = NULL;
+ if (!descriptor) {
+ static const char *valueNames =
+ "Get\000Set\000Result\000Error\000";
+ static const int32_t values[] = {
+ GtalkIqStanza_IqType_Get,
+ GtalkIqStanza_IqType_Set,
+ GtalkIqStanza_IqType_Result,
+ GtalkIqStanza_IqType_Error,
+ };
+ GPBEnumDescriptor *worker =
+ [GPBEnumDescriptor allocDescriptorForName:GPBNSStringifySymbol(GtalkIqStanza_IqType)
+ valueNames:valueNames
+ values:values
+ count:(uint32_t)(sizeof(values) / sizeof(int32_t))
+ enumVerifier:GtalkIqStanza_IqType_IsValidValue];
+ if (!OSAtomicCompareAndSwapPtrBarrier(nil, worker, (void * volatile *)&descriptor)) {
+ [worker release];
+ }
+ }
+ return descriptor;
+}
+
+BOOL GtalkIqStanza_IqType_IsValidValue(int32_t value__) {
+ switch (value__) {
+ case GtalkIqStanza_IqType_Get:
+ case GtalkIqStanza_IqType_Set:
+ case GtalkIqStanza_IqType_Result:
+ case GtalkIqStanza_IqType_Error:
+ return YES;
+ default:
+ return NO;
+ }
+}
+
+#pragma mark - GtalkAppData
+
+@implementation GtalkAppData
+
+@dynamic hasKey, key;
+@dynamic hasValue, value;
+
+typedef struct GtalkAppData__storage_ {
+ uint32_t _has_storage_[1];
+ NSString *key;
+ NSString *value;
+} GtalkAppData__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "key",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkAppData_FieldNumber_Key,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(GtalkAppData__storage_, key),
+ .flags = GPBFieldRequired,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "value",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkAppData_FieldNumber_Value,
+ .hasIndex = 1,
+ .offset = (uint32_t)offsetof(GtalkAppData__storage_, value),
+ .flags = GPBFieldRequired,
+ .dataType = GPBDataTypeString,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GtalkAppData class]
+ rootClass:[GtalkGtalkCoreRoot class]
+ file:GtalkGtalkCoreRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GtalkAppData__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+#pragma mark - GtalkDataMessageStanza
+
+@implementation GtalkDataMessageStanza
+
+@dynamic hasRmqId, rmqId;
+@dynamic hasId_p, id_p;
+@dynamic hasFrom, from;
+@dynamic hasTo, to;
+@dynamic hasCategory, category;
+@dynamic hasToken, token;
+@dynamic appDataArray, appDataArray_Count;
+@dynamic hasFromTrustedServer, fromTrustedServer;
+@dynamic hasPersistentId, persistentId;
+@dynamic hasStreamId, streamId;
+@dynamic hasLastStreamIdReceived, lastStreamIdReceived;
+@dynamic hasPermission, permission;
+@dynamic hasRegId, regId;
+@dynamic hasPkgSignature, pkgSignature;
+@dynamic hasClientId, clientId;
+@dynamic hasDeviceUserId, deviceUserId;
+@dynamic hasTtl, ttl;
+@dynamic hasSent, sent;
+@dynamic hasQueued, queued;
+@dynamic hasStatus, status;
+@dynamic hasRawData, rawData;
+@dynamic hasMaxDelay, maxDelay;
+@dynamic hasActualDelay, actualDelay;
+@dynamic hasImmediateAck, immediateAck;
+@dynamic hasDeliveryReceiptRequested, deliveryReceiptRequested;
+@dynamic hasExternalMessageId, externalMessageId;
+@dynamic hasFlags, flags;
+@dynamic hasCellTower, cellTower;
+@dynamic hasPriority, priority;
+
+typedef struct GtalkDataMessageStanza__storage_ {
+ uint32_t _has_storage_[1];
+ int32_t streamId;
+ int32_t lastStreamIdReceived;
+ int32_t ttl;
+ int32_t queued;
+ int32_t maxDelay;
+ int32_t actualDelay;
+ int32_t priority;
+ NSString *id_p;
+ NSString *from;
+ NSString *to;
+ NSString *category;
+ NSString *token;
+ NSMutableArray *appDataArray;
+ NSString *persistentId;
+ NSString *permission;
+ NSString *regId;
+ NSString *pkgSignature;
+ NSString *clientId;
+ NSData *rawData;
+ NSString *externalMessageId;
+ GtalkCellTower *cellTower;
+ int64_t rmqId;
+ int64_t deviceUserId;
+ int64_t sent;
+ int64_t status;
+ int64_t flags;
+} GtalkDataMessageStanza__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "rmqId",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkDataMessageStanza_FieldNumber_RmqId,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(GtalkDataMessageStanza__storage_, rmqId),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt64,
+ },
+ {
+ .name = "id_p",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkDataMessageStanza_FieldNumber_Id_p,
+ .hasIndex = 1,
+ .offset = (uint32_t)offsetof(GtalkDataMessageStanza__storage_, id_p),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "from",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkDataMessageStanza_FieldNumber_From,
+ .hasIndex = 2,
+ .offset = (uint32_t)offsetof(GtalkDataMessageStanza__storage_, from),
+ .flags = GPBFieldRequired,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "to",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkDataMessageStanza_FieldNumber_To,
+ .hasIndex = 3,
+ .offset = (uint32_t)offsetof(GtalkDataMessageStanza__storage_, to),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "category",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkDataMessageStanza_FieldNumber_Category,
+ .hasIndex = 4,
+ .offset = (uint32_t)offsetof(GtalkDataMessageStanza__storage_, category),
+ .flags = GPBFieldRequired,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "token",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkDataMessageStanza_FieldNumber_Token,
+ .hasIndex = 5,
+ .offset = (uint32_t)offsetof(GtalkDataMessageStanza__storage_, token),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "appDataArray",
+ .dataTypeSpecific.className = GPBStringifySymbol(GtalkAppData),
+ .number = GtalkDataMessageStanza_FieldNumber_AppDataArray,
+ .hasIndex = GPBNoHasBit,
+ .offset = (uint32_t)offsetof(GtalkDataMessageStanza__storage_, appDataArray),
+ .flags = GPBFieldRepeated,
+ .dataType = GPBDataTypeMessage,
+ },
+ {
+ .name = "fromTrustedServer",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkDataMessageStanza_FieldNumber_FromTrustedServer,
+ .hasIndex = 6,
+ .offset = 7, // Stored in _has_storage_ to save space.
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeBool,
+ },
+ {
+ .name = "persistentId",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkDataMessageStanza_FieldNumber_PersistentId,
+ .hasIndex = 8,
+ .offset = (uint32_t)offsetof(GtalkDataMessageStanza__storage_, persistentId),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "streamId",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkDataMessageStanza_FieldNumber_StreamId,
+ .hasIndex = 9,
+ .offset = (uint32_t)offsetof(GtalkDataMessageStanza__storage_, streamId),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt32,
+ },
+ {
+ .name = "lastStreamIdReceived",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkDataMessageStanza_FieldNumber_LastStreamIdReceived,
+ .hasIndex = 10,
+ .offset = (uint32_t)offsetof(GtalkDataMessageStanza__storage_, lastStreamIdReceived),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt32,
+ },
+ {
+ .name = "permission",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkDataMessageStanza_FieldNumber_Permission,
+ .hasIndex = 11,
+ .offset = (uint32_t)offsetof(GtalkDataMessageStanza__storage_, permission),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "regId",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkDataMessageStanza_FieldNumber_RegId,
+ .hasIndex = 12,
+ .offset = (uint32_t)offsetof(GtalkDataMessageStanza__storage_, regId),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "pkgSignature",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkDataMessageStanza_FieldNumber_PkgSignature,
+ .hasIndex = 13,
+ .offset = (uint32_t)offsetof(GtalkDataMessageStanza__storage_, pkgSignature),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "clientId",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkDataMessageStanza_FieldNumber_ClientId,
+ .hasIndex = 14,
+ .offset = (uint32_t)offsetof(GtalkDataMessageStanza__storage_, clientId),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "deviceUserId",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkDataMessageStanza_FieldNumber_DeviceUserId,
+ .hasIndex = 15,
+ .offset = (uint32_t)offsetof(GtalkDataMessageStanza__storage_, deviceUserId),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt64,
+ },
+ {
+ .name = "ttl",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkDataMessageStanza_FieldNumber_Ttl,
+ .hasIndex = 16,
+ .offset = (uint32_t)offsetof(GtalkDataMessageStanza__storage_, ttl),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt32,
+ },
+ {
+ .name = "sent",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkDataMessageStanza_FieldNumber_Sent,
+ .hasIndex = 17,
+ .offset = (uint32_t)offsetof(GtalkDataMessageStanza__storage_, sent),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt64,
+ },
+ {
+ .name = "queued",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkDataMessageStanza_FieldNumber_Queued,
+ .hasIndex = 18,
+ .offset = (uint32_t)offsetof(GtalkDataMessageStanza__storage_, queued),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt32,
+ },
+ {
+ .name = "status",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkDataMessageStanza_FieldNumber_Status,
+ .hasIndex = 19,
+ .offset = (uint32_t)offsetof(GtalkDataMessageStanza__storage_, status),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt64,
+ },
+ {
+ .name = "rawData",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkDataMessageStanza_FieldNumber_RawData,
+ .hasIndex = 20,
+ .offset = (uint32_t)offsetof(GtalkDataMessageStanza__storage_, rawData),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeBytes,
+ },
+ {
+ .name = "maxDelay",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkDataMessageStanza_FieldNumber_MaxDelay,
+ .hasIndex = 21,
+ .offset = (uint32_t)offsetof(GtalkDataMessageStanza__storage_, maxDelay),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt32,
+ },
+ {
+ .name = "actualDelay",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkDataMessageStanza_FieldNumber_ActualDelay,
+ .hasIndex = 22,
+ .offset = (uint32_t)offsetof(GtalkDataMessageStanza__storage_, actualDelay),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt32,
+ },
+ {
+ .name = "immediateAck",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkDataMessageStanza_FieldNumber_ImmediateAck,
+ .hasIndex = 23,
+ .offset = 24, // Stored in _has_storage_ to save space.
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeBool,
+ },
+ {
+ .name = "deliveryReceiptRequested",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkDataMessageStanza_FieldNumber_DeliveryReceiptRequested,
+ .hasIndex = 25,
+ .offset = 26, // Stored in _has_storage_ to save space.
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeBool,
+ },
+ {
+ .name = "externalMessageId",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkDataMessageStanza_FieldNumber_ExternalMessageId,
+ .hasIndex = 27,
+ .offset = (uint32_t)offsetof(GtalkDataMessageStanza__storage_, externalMessageId),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "flags",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkDataMessageStanza_FieldNumber_Flags,
+ .hasIndex = 28,
+ .offset = (uint32_t)offsetof(GtalkDataMessageStanza__storage_, flags),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt64,
+ },
+ {
+ .name = "cellTower",
+ .dataTypeSpecific.className = GPBStringifySymbol(GtalkCellTower),
+ .number = GtalkDataMessageStanza_FieldNumber_CellTower,
+ .hasIndex = 29,
+ .offset = (uint32_t)offsetof(GtalkDataMessageStanza__storage_, cellTower),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeMessage,
+ },
+ {
+ .name = "priority",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkDataMessageStanza_FieldNumber_Priority,
+ .hasIndex = 30,
+ .offset = (uint32_t)offsetof(GtalkDataMessageStanza__storage_, priority),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt32,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GtalkDataMessageStanza class]
+ rootClass:[GtalkGtalkCoreRoot class]
+ file:GtalkGtalkCoreRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GtalkDataMessageStanza__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+#pragma mark - GtalkTalkMetadata
+
+@implementation GtalkTalkMetadata
+
+@dynamic hasForeground, foreground;
+
+typedef struct GtalkTalkMetadata__storage_ {
+ uint32_t _has_storage_[1];
+} GtalkTalkMetadata__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "foreground",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkTalkMetadata_FieldNumber_Foreground,
+ .hasIndex = 0,
+ .offset = 1, // Stored in _has_storage_ to save space.
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeBool,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GtalkTalkMetadata class]
+ rootClass:[GtalkGtalkCoreRoot class]
+ file:GtalkGtalkCoreRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GtalkTalkMetadata__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+#pragma mark - GtalkCellTower
+
+@implementation GtalkCellTower
+
+@dynamic hasId_p, id_p;
+@dynamic hasKnownCongestionStatus, knownCongestionStatus;
+
+typedef struct GtalkCellTower__storage_ {
+ uint32_t _has_storage_[1];
+ int32_t knownCongestionStatus;
+ NSString *id_p;
+} GtalkCellTower__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "id_p",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkCellTower_FieldNumber_Id_p,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(GtalkCellTower__storage_, id_p),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "knownCongestionStatus",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkCellTower_FieldNumber_KnownCongestionStatus,
+ .hasIndex = 1,
+ .offset = (uint32_t)offsetof(GtalkCellTower__storage_, knownCongestionStatus),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt32,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GtalkCellTower class]
+ rootClass:[GtalkGtalkCoreRoot class]
+ file:GtalkGtalkCoreRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GtalkCellTower__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+#pragma mark - GtalkClientEvent
+
+@implementation GtalkClientEvent
+
+@dynamic hasType, type;
+@dynamic hasNumberDiscardedEvents, numberDiscardedEvents;
+@dynamic hasNetworkType, networkType;
+@dynamic hasNetworkPort, networkPort;
+@dynamic hasTimeConnectionStartedMs, timeConnectionStartedMs;
+@dynamic hasTimeConnectionEndedMs, timeConnectionEndedMs;
+@dynamic hasErrorCode, errorCode;
+@dynamic hasTimeConnectionEstablishedMs, timeConnectionEstablishedMs;
+
+typedef struct GtalkClientEvent__storage_ {
+ uint32_t _has_storage_[1];
+ GtalkClientEvent_Type type;
+ uint32_t numberDiscardedEvents;
+ int32_t networkType;
+ int32_t networkPort;
+ int32_t errorCode;
+ uint64_t timeConnectionStartedMs;
+ uint64_t timeConnectionEndedMs;
+ uint64_t timeConnectionEstablishedMs;
+} GtalkClientEvent__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "type",
+ .dataTypeSpecific.enumDescFunc = GtalkClientEvent_Type_EnumDescriptor,
+ .number = GtalkClientEvent_FieldNumber_Type,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(GtalkClientEvent__storage_, type),
+ .flags = (GPBFieldFlags)(GPBFieldOptional | GPBFieldHasEnumDescriptor),
+ .dataType = GPBDataTypeEnum,
+ },
+ {
+ .name = "numberDiscardedEvents",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkClientEvent_FieldNumber_NumberDiscardedEvents,
+ .hasIndex = 1,
+ .offset = (uint32_t)offsetof(GtalkClientEvent__storage_, numberDiscardedEvents),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeUInt32,
+ },
+ {
+ .name = "networkType",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkClientEvent_FieldNumber_NetworkType,
+ .hasIndex = 2,
+ .offset = (uint32_t)offsetof(GtalkClientEvent__storage_, networkType),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt32,
+ },
+ {
+ .name = "networkPort",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkClientEvent_FieldNumber_NetworkPort,
+ .hasIndex = 3,
+ .offset = (uint32_t)offsetof(GtalkClientEvent__storage_, networkPort),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt32,
+ },
+ {
+ .name = "timeConnectionStartedMs",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkClientEvent_FieldNumber_TimeConnectionStartedMs,
+ .hasIndex = 4,
+ .offset = (uint32_t)offsetof(GtalkClientEvent__storage_, timeConnectionStartedMs),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeUInt64,
+ },
+ {
+ .name = "timeConnectionEndedMs",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkClientEvent_FieldNumber_TimeConnectionEndedMs,
+ .hasIndex = 5,
+ .offset = (uint32_t)offsetof(GtalkClientEvent__storage_, timeConnectionEndedMs),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeUInt64,
+ },
+ {
+ .name = "errorCode",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkClientEvent_FieldNumber_ErrorCode,
+ .hasIndex = 6,
+ .offset = (uint32_t)offsetof(GtalkClientEvent__storage_, errorCode),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt32,
+ },
+ {
+ .name = "timeConnectionEstablishedMs",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkClientEvent_FieldNumber_TimeConnectionEstablishedMs,
+ .hasIndex = 7,
+ .offset = (uint32_t)offsetof(GtalkClientEvent__storage_, timeConnectionEstablishedMs),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeUInt64,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GtalkClientEvent class]
+ rootClass:[GtalkGtalkCoreRoot class]
+ file:GtalkGtalkCoreRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GtalkClientEvent__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+#pragma mark - Enum GtalkClientEvent_Type
+
+GPBEnumDescriptor *GtalkClientEvent_Type_EnumDescriptor(void) {
+ static GPBEnumDescriptor *descriptor = NULL;
+ if (!descriptor) {
+ static const char *valueNames =
+ "Unknown\000DiscardedEvents\000FailedConnection"
+ "\000SuccessfulConnection\000";
+ static const int32_t values[] = {
+ GtalkClientEvent_Type_Unknown,
+ GtalkClientEvent_Type_DiscardedEvents,
+ GtalkClientEvent_Type_FailedConnection,
+ GtalkClientEvent_Type_SuccessfulConnection,
+ };
+ GPBEnumDescriptor *worker =
+ [GPBEnumDescriptor allocDescriptorForName:GPBNSStringifySymbol(GtalkClientEvent_Type)
+ valueNames:valueNames
+ values:values
+ count:(uint32_t)(sizeof(values) / sizeof(int32_t))
+ enumVerifier:GtalkClientEvent_Type_IsValidValue];
+ if (!OSAtomicCompareAndSwapPtrBarrier(nil, worker, (void * volatile *)&descriptor)) {
+ [worker release];
+ }
+ }
+ return descriptor;
+}
+
+BOOL GtalkClientEvent_Type_IsValidValue(int32_t value__) {
+ switch (value__) {
+ case GtalkClientEvent_Type_Unknown:
+ case GtalkClientEvent_Type_DiscardedEvents:
+ case GtalkClientEvent_Type_FailedConnection:
+ case GtalkClientEvent_Type_SuccessfulConnection:
+ return YES;
+ default:
+ return NO;
+ }
+}
+
+
+#pragma clang diagnostic pop
+
+// @@protoc_insertion_point(global_scope)
diff --git a/Firebase/Messaging/Protos/GtalkExtensions.pbobjc.h b/Firebase/Messaging/Protos/GtalkExtensions.pbobjc.h
new file mode 100644
index 0000000..f461884
--- /dev/null
+++ b/Firebase/Messaging/Protos/GtalkExtensions.pbobjc.h
@@ -0,0 +1,617 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Generated by the protocol buffer compiler. DO NOT EDIT!
+// source: buzz/mobile/proto/gtalk_extensions.proto
+
+// This CPP symbol can be defined to use imports that match up to the framework
+// imports needed when using CocoaPods.
+#if !defined(GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS)
+ #define GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS 0
+#endif
+
+#if GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS
+ #import <Protobuf/GPBProtocolBuffers.h>
+#else
+ #import "GPBProtocolBuffers.h"
+#endif
+
+#if GOOGLE_PROTOBUF_OBJC_VERSION < 30002
+#error This file was generated by a newer version of protoc which is incompatible with your Protocol Buffer library sources.
+#endif
+#if 30002 < GOOGLE_PROTOBUF_OBJC_MIN_SUPPORTED_VERSION
+#error This file was generated by an older version of protoc which is incompatible with your Protocol Buffer library sources.
+#endif
+
+// @@protoc_insertion_point(imports)
+
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdeprecated-declarations"
+
+CF_EXTERN_C_BEGIN
+
+@class GtalkOtrItem;
+@class GtalkPhoto;
+@class GtalkRosterItem;
+@class GtalkSharedStatus_StatusList;
+
+NS_ASSUME_NONNULL_BEGIN
+
+#pragma mark - Enum GtalkRosterItem_SubscriptionType
+
+typedef GPB_ENUM(GtalkRosterItem_SubscriptionType) {
+ GtalkRosterItem_SubscriptionType_None = 0,
+ GtalkRosterItem_SubscriptionType_To = 1,
+ GtalkRosterItem_SubscriptionType_From = 2,
+ GtalkRosterItem_SubscriptionType_Both = 3,
+ GtalkRosterItem_SubscriptionType_Remove = 4,
+};
+
+GPBEnumDescriptor *GtalkRosterItem_SubscriptionType_EnumDescriptor(void);
+
+/**
+ * Checks to see if the given value is defined by the enum or was not known at
+ * the time this source was generated.
+ **/
+BOOL GtalkRosterItem_SubscriptionType_IsValidValue(int32_t value);
+
+#pragma mark - Enum GtalkRosterItem_AskType
+
+typedef GPB_ENUM(GtalkRosterItem_AskType) {
+ GtalkRosterItem_AskType_Subscribe = 0,
+};
+
+GPBEnumDescriptor *GtalkRosterItem_AskType_EnumDescriptor(void);
+
+/**
+ * Checks to see if the given value is defined by the enum or was not known at
+ * the time this source was generated.
+ **/
+BOOL GtalkRosterItem_AskType_IsValidValue(int32_t value);
+
+#pragma mark - Enum GtalkRosterItem_DisplayType
+
+typedef GPB_ENUM(GtalkRosterItem_DisplayType) {
+ GtalkRosterItem_DisplayType_Blocked = 0,
+ GtalkRosterItem_DisplayType_Hidden = 1,
+ GtalkRosterItem_DisplayType_Pinned = 2,
+};
+
+GPBEnumDescriptor *GtalkRosterItem_DisplayType_EnumDescriptor(void);
+
+/**
+ * Checks to see if the given value is defined by the enum or was not known at
+ * the time this source was generated.
+ **/
+BOOL GtalkRosterItem_DisplayType_IsValidValue(int32_t value);
+
+#pragma mark - Enum GtalkSharedStatus_ShowType
+
+typedef GPB_ENUM(GtalkSharedStatus_ShowType) {
+ GtalkSharedStatus_ShowType_Default = 0,
+ GtalkSharedStatus_ShowType_Dnd = 1,
+};
+
+GPBEnumDescriptor *GtalkSharedStatus_ShowType_EnumDescriptor(void);
+
+/**
+ * Checks to see if the given value is defined by the enum or was not known at
+ * the time this source was generated.
+ **/
+BOOL GtalkSharedStatus_ShowType_IsValidValue(int32_t value);
+
+#pragma mark - Enum GtalkPostAuthBatchQuery_CapabilitiesExtFlags
+
+typedef GPB_ENUM(GtalkPostAuthBatchQuery_CapabilitiesExtFlags) {
+ GtalkPostAuthBatchQuery_CapabilitiesExtFlags_HasVoiceV1 = 1,
+ GtalkPostAuthBatchQuery_CapabilitiesExtFlags_HasVideoV1 = 2,
+ GtalkPostAuthBatchQuery_CapabilitiesExtFlags_HasCameraV1 = 4,
+ GtalkPostAuthBatchQuery_CapabilitiesExtFlags_HasPmucV1 = 8,
+};
+
+GPBEnumDescriptor *GtalkPostAuthBatchQuery_CapabilitiesExtFlags_EnumDescriptor(void);
+
+/**
+ * Checks to see if the given value is defined by the enum or was not known at
+ * the time this source was generated.
+ **/
+BOOL GtalkPostAuthBatchQuery_CapabilitiesExtFlags_IsValidValue(int32_t value);
+
+#pragma mark - GtalkGtalkExtensionsRoot
+
+/**
+ * Exposes the extension registry for this file.
+ *
+ * The base class provides:
+ * @code
+ * + (GPBExtensionRegistry *)extensionRegistry;
+ * @endcode
+ * which is a @c GPBExtensionRegistry that includes all the extensions defined by
+ * this file and all files that it depends on.
+ **/
+@interface GtalkGtalkExtensionsRoot : GPBRootObject
+@end
+
+#pragma mark - GtalkRosterQuery
+
+typedef GPB_ENUM(GtalkRosterQuery_FieldNumber) {
+ GtalkRosterQuery_FieldNumber_Etag = 1,
+ GtalkRosterQuery_FieldNumber_NotModified = 2,
+ GtalkRosterQuery_FieldNumber_ItemArray = 3,
+ GtalkRosterQuery_FieldNumber_AvatarWidth = 4,
+ GtalkRosterQuery_FieldNumber_AvatarHeight = 5,
+};
+
+@interface GtalkRosterQuery : GPBMessage
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *etag;
+/** Test to see if @c etag has been set. */
+@property(nonatomic, readwrite) BOOL hasEtag;
+
+
+@property(nonatomic, readwrite) BOOL notModified;
+
+@property(nonatomic, readwrite) BOOL hasNotModified;
+
+@property(nonatomic, readwrite, strong, null_resettable) NSMutableArray<GtalkRosterItem*> *itemArray;
+/** The number of items in @c itemArray without causing the array to be created. */
+@property(nonatomic, readonly) NSUInteger itemArray_Count;
+
+
+@property(nonatomic, readwrite) int32_t avatarWidth;
+
+@property(nonatomic, readwrite) BOOL hasAvatarWidth;
+
+@property(nonatomic, readwrite) int32_t avatarHeight;
+
+@property(nonatomic, readwrite) BOOL hasAvatarHeight;
+@end
+
+#pragma mark - GtalkRosterItem
+
+typedef GPB_ENUM(GtalkRosterItem_FieldNumber) {
+ GtalkRosterItem_FieldNumber_Jid = 1,
+ GtalkRosterItem_FieldNumber_Name = 2,
+ GtalkRosterItem_FieldNumber_Subscription = 3,
+ GtalkRosterItem_FieldNumber_Ask = 4,
+ GtalkRosterItem_FieldNumber_GroupArray = 5,
+ GtalkRosterItem_FieldNumber_QuickContact = 6,
+ GtalkRosterItem_FieldNumber_Display = 7,
+ GtalkRosterItem_FieldNumber_Rejected = 8,
+};
+
+@interface GtalkRosterItem : GPBMessage
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *jid;
+/** Test to see if @c jid has been set. */
+@property(nonatomic, readwrite) BOOL hasJid;
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *name;
+/** Test to see if @c name has been set. */
+@property(nonatomic, readwrite) BOOL hasName;
+
+
+@property(nonatomic, readwrite) GtalkRosterItem_SubscriptionType subscription;
+
+@property(nonatomic, readwrite) BOOL hasSubscription;
+
+@property(nonatomic, readwrite) GtalkRosterItem_AskType ask;
+
+@property(nonatomic, readwrite) BOOL hasAsk;
+
+@property(nonatomic, readwrite, strong, null_resettable) NSMutableArray<NSString*> *groupArray;
+/** The number of items in @c groupArray without causing the array to be created. */
+@property(nonatomic, readonly) NSUInteger groupArray_Count;
+
+
+@property(nonatomic, readwrite) BOOL quickContact;
+
+@property(nonatomic, readwrite) BOOL hasQuickContact;
+
+@property(nonatomic, readwrite) GtalkRosterItem_DisplayType display;
+
+@property(nonatomic, readwrite) BOOL hasDisplay;
+
+@property(nonatomic, readwrite) BOOL rejected;
+
+@property(nonatomic, readwrite) BOOL hasRejected;
+@end
+
+#pragma mark - GtalkRmqLastId
+
+typedef GPB_ENUM(GtalkRmqLastId_FieldNumber) {
+ GtalkRmqLastId_FieldNumber_Id_p = 1,
+};
+
+@interface GtalkRmqLastId : GPBMessage
+
+
+@property(nonatomic, readwrite) int64_t id_p;
+
+@property(nonatomic, readwrite) BOOL hasId_p;
+@end
+
+#pragma mark - GtalkRmqAck
+
+typedef GPB_ENUM(GtalkRmqAck_FieldNumber) {
+ GtalkRmqAck_FieldNumber_Id_p = 1,
+};
+
+@interface GtalkRmqAck : GPBMessage
+
+
+@property(nonatomic, readwrite) int64_t id_p;
+
+@property(nonatomic, readwrite) BOOL hasId_p;
+@end
+
+#pragma mark - GtalkVCard
+
+typedef GPB_ENUM(GtalkVCard_FieldNumber) {
+ GtalkVCard_FieldNumber_Version = 1,
+ GtalkVCard_FieldNumber_FullName = 2,
+ GtalkVCard_FieldNumber_Photo = 3,
+ GtalkVCard_FieldNumber_AvatarHash = 4,
+ GtalkVCard_FieldNumber_Modified = 5,
+};
+
+@interface GtalkVCard : GPBMessage
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *version;
+/** Test to see if @c version has been set. */
+@property(nonatomic, readwrite) BOOL hasVersion;
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *fullName;
+/** Test to see if @c fullName has been set. */
+@property(nonatomic, readwrite) BOOL hasFullName;
+
+
+@property(nonatomic, readwrite, strong, null_resettable) GtalkPhoto *photo;
+/** Test to see if @c photo has been set. */
+@property(nonatomic, readwrite) BOOL hasPhoto;
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *avatarHash;
+/** Test to see if @c avatarHash has been set. */
+@property(nonatomic, readwrite) BOOL hasAvatarHash;
+
+
+@property(nonatomic, readwrite) BOOL modified;
+
+@property(nonatomic, readwrite) BOOL hasModified;
+@end
+
+#pragma mark - GtalkPhoto
+
+typedef GPB_ENUM(GtalkPhoto_FieldNumber) {
+ GtalkPhoto_FieldNumber_Type = 1,
+ GtalkPhoto_FieldNumber_Data_p = 2,
+};
+
+@interface GtalkPhoto : GPBMessage
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *type;
+/** Test to see if @c type has been set. */
+@property(nonatomic, readwrite) BOOL hasType;
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *data_p;
+/** Test to see if @c data_p has been set. */
+@property(nonatomic, readwrite) BOOL hasData_p;
+
+@end
+
+#pragma mark - GtalkChatRead
+
+typedef GPB_ENUM(GtalkChatRead_FieldNumber) {
+ GtalkChatRead_FieldNumber_User = 1,
+};
+
+@interface GtalkChatRead : GPBMessage
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *user;
+/** Test to see if @c user has been set. */
+@property(nonatomic, readwrite) BOOL hasUser;
+
+@end
+
+#pragma mark - GtalkChatClosed
+
+typedef GPB_ENUM(GtalkChatClosed_FieldNumber) {
+ GtalkChatClosed_FieldNumber_User = 1,
+};
+
+@interface GtalkChatClosed : GPBMessage
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *user;
+/** Test to see if @c user has been set. */
+@property(nonatomic, readwrite) BOOL hasUser;
+
+@end
+
+#pragma mark - GtalkCapabilities
+
+typedef GPB_ENUM(GtalkCapabilities_FieldNumber) {
+ GtalkCapabilities_FieldNumber_Node = 1,
+ GtalkCapabilities_FieldNumber_Ver = 2,
+ GtalkCapabilities_FieldNumber_Ext = 3,
+ GtalkCapabilities_FieldNumber_Hash_p = 4,
+};
+
+@interface GtalkCapabilities : GPBMessage
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *node;
+/** Test to see if @c node has been set. */
+@property(nonatomic, readwrite) BOOL hasNode;
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *ver;
+/** Test to see if @c ver has been set. */
+@property(nonatomic, readwrite) BOOL hasVer;
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *ext;
+/** Test to see if @c ext has been set. */
+@property(nonatomic, readwrite) BOOL hasExt;
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *hash_p;
+/** Test to see if @c hash_p has been set. */
+@property(nonatomic, readwrite) BOOL hasHash_p;
+
+@end
+
+#pragma mark - GtalkSharedStatus
+
+typedef GPB_ENUM(GtalkSharedStatus_FieldNumber) {
+ GtalkSharedStatus_FieldNumber_StatusMax = 1,
+ GtalkSharedStatus_FieldNumber_StatusListMax = 2,
+ GtalkSharedStatus_FieldNumber_StatusListContentsMax = 3,
+ GtalkSharedStatus_FieldNumber_Status = 4,
+ GtalkSharedStatus_FieldNumber_Show = 5,
+ GtalkSharedStatus_FieldNumber_StatusListArray = 6,
+ GtalkSharedStatus_FieldNumber_Invisible = 9,
+ GtalkSharedStatus_FieldNumber_StatusMinVersion = 10,
+};
+
+@interface GtalkSharedStatus : GPBMessage
+
+
+@property(nonatomic, readwrite) int32_t statusMax;
+
+@property(nonatomic, readwrite) BOOL hasStatusMax;
+
+@property(nonatomic, readwrite) int32_t statusListMax;
+
+@property(nonatomic, readwrite) BOOL hasStatusListMax;
+
+@property(nonatomic, readwrite) int32_t statusListContentsMax;
+
+@property(nonatomic, readwrite) BOOL hasStatusListContentsMax;
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *status;
+/** Test to see if @c status has been set. */
+@property(nonatomic, readwrite) BOOL hasStatus;
+
+
+@property(nonatomic, readwrite) GtalkSharedStatus_ShowType show;
+
+@property(nonatomic, readwrite) BOOL hasShow;
+
+@property(nonatomic, readwrite, strong, null_resettable) NSMutableArray<GtalkSharedStatus_StatusList*> *statusListArray;
+/** The number of items in @c statusListArray without causing the array to be created. */
+@property(nonatomic, readonly) NSUInteger statusListArray_Count;
+
+
+@property(nonatomic, readwrite) BOOL invisible;
+
+@property(nonatomic, readwrite) BOOL hasInvisible;
+
+@property(nonatomic, readwrite) int32_t statusMinVersion;
+
+@property(nonatomic, readwrite) BOOL hasStatusMinVersion;
+@end
+
+#pragma mark - GtalkSharedStatus_StatusList
+
+typedef GPB_ENUM(GtalkSharedStatus_StatusList_FieldNumber) {
+ GtalkSharedStatus_StatusList_FieldNumber_Show = 7,
+ GtalkSharedStatus_StatusList_FieldNumber_StatusArray = 8,
+};
+
+@interface GtalkSharedStatus_StatusList : GPBMessage
+
+
+@property(nonatomic, readwrite) GtalkSharedStatus_ShowType show;
+
+@property(nonatomic, readwrite) BOOL hasShow;
+
+@property(nonatomic, readwrite, strong, null_resettable) NSMutableArray<NSString*> *statusArray;
+/** The number of items in @c statusArray without causing the array to be created. */
+@property(nonatomic, readonly) NSUInteger statusArray_Count;
+
+@end
+
+#pragma mark - GtalkOtrQuery
+
+typedef GPB_ENUM(GtalkOtrQuery_FieldNumber) {
+ GtalkOtrQuery_FieldNumber_NosaveDefault = 1,
+ GtalkOtrQuery_FieldNumber_ItemArray = 2,
+ GtalkOtrQuery_FieldNumber_Etag = 3,
+ GtalkOtrQuery_FieldNumber_NotModified = 4,
+};
+
+@interface GtalkOtrQuery : GPBMessage
+
+
+@property(nonatomic, readwrite) BOOL nosaveDefault;
+
+@property(nonatomic, readwrite) BOOL hasNosaveDefault;
+
+@property(nonatomic, readwrite, strong, null_resettable) NSMutableArray<GtalkOtrItem*> *itemArray;
+/** The number of items in @c itemArray without causing the array to be created. */
+@property(nonatomic, readonly) NSUInteger itemArray_Count;
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *etag;
+/** Test to see if @c etag has been set. */
+@property(nonatomic, readwrite) BOOL hasEtag;
+
+
+@property(nonatomic, readwrite) BOOL notModified;
+
+@property(nonatomic, readwrite) BOOL hasNotModified;
+@end
+
+#pragma mark - GtalkOtrItem
+
+typedef GPB_ENUM(GtalkOtrItem_FieldNumber) {
+ GtalkOtrItem_FieldNumber_Jid = 1,
+ GtalkOtrItem_FieldNumber_Nosave = 2,
+ GtalkOtrItem_FieldNumber_ChangedByBuddy = 3,
+};
+
+@interface GtalkOtrItem : GPBMessage
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *jid;
+/** Test to see if @c jid has been set. */
+@property(nonatomic, readwrite) BOOL hasJid;
+
+
+@property(nonatomic, readwrite) BOOL nosave;
+
+@property(nonatomic, readwrite) BOOL hasNosave;
+
+@property(nonatomic, readwrite) BOOL changedByBuddy;
+
+@property(nonatomic, readwrite) BOOL hasChangedByBuddy;
+@end
+
+#pragma mark - GtalkIdle
+
+typedef GPB_ENUM(GtalkIdle_FieldNumber) {
+ GtalkIdle_FieldNumber_Idle = 1,
+ GtalkIdle_FieldNumber_Away = 2,
+};
+
+@interface GtalkIdle : GPBMessage
+
+
+@property(nonatomic, readwrite) BOOL idle;
+
+@property(nonatomic, readwrite) BOOL hasIdle;
+
+@property(nonatomic, readwrite) BOOL away;
+
+@property(nonatomic, readwrite) BOOL hasAway;
+@end
+
+#pragma mark - GtalkPostAuthBatchQuery
+
+typedef GPB_ENUM(GtalkPostAuthBatchQuery_FieldNumber) {
+ GtalkPostAuthBatchQuery_FieldNumber_Available = 1,
+ GtalkPostAuthBatchQuery_FieldNumber_DeviceIdle = 2,
+ GtalkPostAuthBatchQuery_FieldNumber_MobileIndicator = 3,
+ GtalkPostAuthBatchQuery_FieldNumber_SharedStatusVersion = 4,
+ GtalkPostAuthBatchQuery_FieldNumber_RosterEtag = 5,
+ GtalkPostAuthBatchQuery_FieldNumber_OtrEtag = 6,
+ GtalkPostAuthBatchQuery_FieldNumber_AvatarHash = 7,
+ GtalkPostAuthBatchQuery_FieldNumber_VcardQueryStanzaId = 8,
+ GtalkPostAuthBatchQuery_FieldNumber_CapabilitiesExtFlags = 9,
+};
+
+@interface GtalkPostAuthBatchQuery : GPBMessage
+
+
+@property(nonatomic, readwrite) BOOL available;
+
+@property(nonatomic, readwrite) BOOL hasAvailable;
+
+@property(nonatomic, readwrite) BOOL deviceIdle;
+
+@property(nonatomic, readwrite) BOOL hasDeviceIdle;
+
+@property(nonatomic, readwrite) BOOL mobileIndicator;
+
+@property(nonatomic, readwrite) BOOL hasMobileIndicator;
+
+@property(nonatomic, readwrite) int32_t sharedStatusVersion;
+
+@property(nonatomic, readwrite) BOOL hasSharedStatusVersion;
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *rosterEtag;
+/** Test to see if @c rosterEtag has been set. */
+@property(nonatomic, readwrite) BOOL hasRosterEtag;
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *otrEtag;
+/** Test to see if @c otrEtag has been set. */
+@property(nonatomic, readwrite) BOOL hasOtrEtag;
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *avatarHash;
+/** Test to see if @c avatarHash has been set. */
+@property(nonatomic, readwrite) BOOL hasAvatarHash;
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *vcardQueryStanzaId;
+/** Test to see if @c vcardQueryStanzaId has been set. */
+@property(nonatomic, readwrite) BOOL hasVcardQueryStanzaId;
+
+
+@property(nonatomic, readwrite) int32_t capabilitiesExtFlags;
+
+@property(nonatomic, readwrite) BOOL hasCapabilitiesExtFlags;
+@end
+
+#pragma mark - GtalkStreamAck
+
+@interface GtalkStreamAck : GPBMessage
+
+@end
+
+#pragma mark - GtalkSelectiveAck
+
+typedef GPB_ENUM(GtalkSelectiveAck_FieldNumber) {
+ GtalkSelectiveAck_FieldNumber_IdArray = 1,
+};
+
+@interface GtalkSelectiveAck : GPBMessage
+
+
+@property(nonatomic, readwrite, strong, null_resettable) NSMutableArray<NSString*> *idArray;
+/** The number of items in @c idArray without causing the array to be created. */
+@property(nonatomic, readonly) NSUInteger idArray_Count;
+
+@end
+
+NS_ASSUME_NONNULL_END
+
+CF_EXTERN_C_END
+
+#pragma clang diagnostic pop
+
+// @@protoc_insertion_point(global_scope)
diff --git a/Firebase/Messaging/Protos/GtalkExtensions.pbobjc.m b/Firebase/Messaging/Protos/GtalkExtensions.pbobjc.m
new file mode 100644
index 0000000..e41d416
--- /dev/null
+++ b/Firebase/Messaging/Protos/GtalkExtensions.pbobjc.m
@@ -0,0 +1,1407 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Generated by the protocol buffer compiler. DO NOT EDIT!
+// source: buzz/mobile/proto/gtalk_extensions.proto
+
+// This CPP symbol can be defined to use imports that match up to the framework
+// imports needed when using CocoaPods.
+#if !defined(GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS)
+ #define GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS 0
+#endif
+
+#if GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS
+ #import <Protobuf/GPBProtocolBuffers_RuntimeSupport.h>
+#else
+ #import "GPBProtocolBuffers_RuntimeSupport.h"
+#endif
+
+ #import "GtalkExtensions.pbobjc.h"
+// @@protoc_insertion_point(imports)
+
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdeprecated-declarations"
+
+#pragma mark - GtalkGtalkExtensionsRoot
+
+@implementation GtalkGtalkExtensionsRoot
+
+// No extensions in the file and no imports, so no need to generate
+// +extensionRegistry.
+
+@end
+
+#pragma mark - GtalkGtalkExtensionsRoot_FileDescriptor
+
+static GPBFileDescriptor *GtalkGtalkExtensionsRoot_FileDescriptor(void) {
+ // This is called by +initialize so there is no need to worry
+ // about thread safety of the singleton.
+ static GPBFileDescriptor *descriptor = NULL;
+ if (!descriptor) {
+ GPB_DEBUG_CHECK_RUNTIME_VERSIONS();
+ descriptor = [[GPBFileDescriptor alloc] initWithPackage:@"mobilegtalk"
+ objcPrefix:@"Gtalk"
+ syntax:GPBFileSyntaxProto2];
+ }
+ return descriptor;
+}
+
+#pragma mark - GtalkRosterQuery
+
+@implementation GtalkRosterQuery
+
+@dynamic hasEtag, etag;
+@dynamic hasNotModified, notModified;
+@dynamic itemArray, itemArray_Count;
+@dynamic hasAvatarWidth, avatarWidth;
+@dynamic hasAvatarHeight, avatarHeight;
+
+typedef struct GtalkRosterQuery__storage_ {
+ uint32_t _has_storage_[1];
+ int32_t avatarWidth;
+ int32_t avatarHeight;
+ NSString *etag;
+ NSMutableArray *itemArray;
+} GtalkRosterQuery__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "etag",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkRosterQuery_FieldNumber_Etag,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(GtalkRosterQuery__storage_, etag),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "notModified",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkRosterQuery_FieldNumber_NotModified,
+ .hasIndex = 1,
+ .offset = 2, // Stored in _has_storage_ to save space.
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeBool,
+ },
+ {
+ .name = "itemArray",
+ .dataTypeSpecific.className = GPBStringifySymbol(GtalkRosterItem),
+ .number = GtalkRosterQuery_FieldNumber_ItemArray,
+ .hasIndex = GPBNoHasBit,
+ .offset = (uint32_t)offsetof(GtalkRosterQuery__storage_, itemArray),
+ .flags = GPBFieldRepeated,
+ .dataType = GPBDataTypeMessage,
+ },
+ {
+ .name = "avatarWidth",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkRosterQuery_FieldNumber_AvatarWidth,
+ .hasIndex = 3,
+ .offset = (uint32_t)offsetof(GtalkRosterQuery__storage_, avatarWidth),
+ .flags = (GPBFieldFlags)(GPBFieldOptional | GPBFieldTextFormatNameCustom),
+ .dataType = GPBDataTypeInt32,
+ },
+ {
+ .name = "avatarHeight",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkRosterQuery_FieldNumber_AvatarHeight,
+ .hasIndex = 4,
+ .offset = (uint32_t)offsetof(GtalkRosterQuery__storage_, avatarHeight),
+ .flags = (GPBFieldFlags)(GPBFieldOptional | GPBFieldTextFormatNameCustom),
+ .dataType = GPBDataTypeInt32,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GtalkRosterQuery class]
+ rootClass:[GtalkGtalkExtensionsRoot class]
+ file:GtalkGtalkExtensionsRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GtalkRosterQuery__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+#if !GPBOBJC_SKIP_MESSAGE_TEXTFORMAT_EXTRAS
+ static const char *extraTextFormatInfo =
+ "\002\004\013\000\005\014\000";
+ [localDescriptor setupExtraTextInfo:extraTextFormatInfo];
+#endif // !GPBOBJC_SKIP_MESSAGE_TEXTFORMAT_EXTRAS
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+#pragma mark - GtalkRosterItem
+
+@implementation GtalkRosterItem
+
+@dynamic hasJid, jid;
+@dynamic hasName, name;
+@dynamic hasSubscription, subscription;
+@dynamic hasAsk, ask;
+@dynamic groupArray, groupArray_Count;
+@dynamic hasQuickContact, quickContact;
+@dynamic hasDisplay, display;
+@dynamic hasRejected, rejected;
+
+typedef struct GtalkRosterItem__storage_ {
+ uint32_t _has_storage_[1];
+ GtalkRosterItem_SubscriptionType subscription;
+ GtalkRosterItem_AskType ask;
+ GtalkRosterItem_DisplayType display;
+ NSString *jid;
+ NSString *name;
+ NSMutableArray *groupArray;
+} GtalkRosterItem__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "jid",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkRosterItem_FieldNumber_Jid,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(GtalkRosterItem__storage_, jid),
+ .flags = GPBFieldRequired,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "name",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkRosterItem_FieldNumber_Name,
+ .hasIndex = 1,
+ .offset = (uint32_t)offsetof(GtalkRosterItem__storage_, name),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "subscription",
+ .dataTypeSpecific.enumDescFunc = GtalkRosterItem_SubscriptionType_EnumDescriptor,
+ .number = GtalkRosterItem_FieldNumber_Subscription,
+ .hasIndex = 2,
+ .offset = (uint32_t)offsetof(GtalkRosterItem__storage_, subscription),
+ .flags = (GPBFieldFlags)(GPBFieldRequired | GPBFieldHasEnumDescriptor),
+ .dataType = GPBDataTypeEnum,
+ },
+ {
+ .name = "ask",
+ .dataTypeSpecific.enumDescFunc = GtalkRosterItem_AskType_EnumDescriptor,
+ .number = GtalkRosterItem_FieldNumber_Ask,
+ .hasIndex = 3,
+ .offset = (uint32_t)offsetof(GtalkRosterItem__storage_, ask),
+ .flags = (GPBFieldFlags)(GPBFieldOptional | GPBFieldHasEnumDescriptor),
+ .dataType = GPBDataTypeEnum,
+ },
+ {
+ .name = "groupArray",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkRosterItem_FieldNumber_GroupArray,
+ .hasIndex = GPBNoHasBit,
+ .offset = (uint32_t)offsetof(GtalkRosterItem__storage_, groupArray),
+ .flags = GPBFieldRepeated,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "quickContact",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkRosterItem_FieldNumber_QuickContact,
+ .hasIndex = 4,
+ .offset = 5, // Stored in _has_storage_ to save space.
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeBool,
+ },
+ {
+ .name = "display",
+ .dataTypeSpecific.enumDescFunc = GtalkRosterItem_DisplayType_EnumDescriptor,
+ .number = GtalkRosterItem_FieldNumber_Display,
+ .hasIndex = 6,
+ .offset = (uint32_t)offsetof(GtalkRosterItem__storage_, display),
+ .flags = (GPBFieldFlags)(GPBFieldOptional | GPBFieldHasEnumDescriptor),
+ .dataType = GPBDataTypeEnum,
+ },
+ {
+ .name = "rejected",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkRosterItem_FieldNumber_Rejected,
+ .hasIndex = 7,
+ .offset = 8, // Stored in _has_storage_ to save space.
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeBool,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GtalkRosterItem class]
+ rootClass:[GtalkGtalkExtensionsRoot class]
+ file:GtalkGtalkExtensionsRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GtalkRosterItem__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+#pragma mark - Enum GtalkRosterItem_SubscriptionType
+
+GPBEnumDescriptor *GtalkRosterItem_SubscriptionType_EnumDescriptor(void) {
+ static GPBEnumDescriptor *descriptor = NULL;
+ if (!descriptor) {
+ static const char *valueNames =
+ "None\000To\000From\000Both\000Remove\000";
+ static const int32_t values[] = {
+ GtalkRosterItem_SubscriptionType_None,
+ GtalkRosterItem_SubscriptionType_To,
+ GtalkRosterItem_SubscriptionType_From,
+ GtalkRosterItem_SubscriptionType_Both,
+ GtalkRosterItem_SubscriptionType_Remove,
+ };
+ GPBEnumDescriptor *worker =
+ [GPBEnumDescriptor allocDescriptorForName:GPBNSStringifySymbol(GtalkRosterItem_SubscriptionType)
+ valueNames:valueNames
+ values:values
+ count:(uint32_t)(sizeof(values) / sizeof(int32_t))
+ enumVerifier:GtalkRosterItem_SubscriptionType_IsValidValue];
+ if (!OSAtomicCompareAndSwapPtrBarrier(nil, worker, (void * volatile *)&descriptor)) {
+ [worker release];
+ }
+ }
+ return descriptor;
+}
+
+BOOL GtalkRosterItem_SubscriptionType_IsValidValue(int32_t value__) {
+ switch (value__) {
+ case GtalkRosterItem_SubscriptionType_None:
+ case GtalkRosterItem_SubscriptionType_To:
+ case GtalkRosterItem_SubscriptionType_From:
+ case GtalkRosterItem_SubscriptionType_Both:
+ case GtalkRosterItem_SubscriptionType_Remove:
+ return YES;
+ default:
+ return NO;
+ }
+}
+
+#pragma mark - Enum GtalkRosterItem_AskType
+
+GPBEnumDescriptor *GtalkRosterItem_AskType_EnumDescriptor(void) {
+ static GPBEnumDescriptor *descriptor = NULL;
+ if (!descriptor) {
+ static const char *valueNames =
+ "Subscribe\000";
+ static const int32_t values[] = {
+ GtalkRosterItem_AskType_Subscribe,
+ };
+ GPBEnumDescriptor *worker =
+ [GPBEnumDescriptor allocDescriptorForName:GPBNSStringifySymbol(GtalkRosterItem_AskType)
+ valueNames:valueNames
+ values:values
+ count:(uint32_t)(sizeof(values) / sizeof(int32_t))
+ enumVerifier:GtalkRosterItem_AskType_IsValidValue];
+ if (!OSAtomicCompareAndSwapPtrBarrier(nil, worker, (void * volatile *)&descriptor)) {
+ [worker release];
+ }
+ }
+ return descriptor;
+}
+
+BOOL GtalkRosterItem_AskType_IsValidValue(int32_t value__) {
+ switch (value__) {
+ case GtalkRosterItem_AskType_Subscribe:
+ return YES;
+ default:
+ return NO;
+ }
+}
+
+#pragma mark - Enum GtalkRosterItem_DisplayType
+
+GPBEnumDescriptor *GtalkRosterItem_DisplayType_EnumDescriptor(void) {
+ static GPBEnumDescriptor *descriptor = NULL;
+ if (!descriptor) {
+ static const char *valueNames =
+ "Blocked\000Hidden\000Pinned\000";
+ static const int32_t values[] = {
+ GtalkRosterItem_DisplayType_Blocked,
+ GtalkRosterItem_DisplayType_Hidden,
+ GtalkRosterItem_DisplayType_Pinned,
+ };
+ GPBEnumDescriptor *worker =
+ [GPBEnumDescriptor allocDescriptorForName:GPBNSStringifySymbol(GtalkRosterItem_DisplayType)
+ valueNames:valueNames
+ values:values
+ count:(uint32_t)(sizeof(values) / sizeof(int32_t))
+ enumVerifier:GtalkRosterItem_DisplayType_IsValidValue];
+ if (!OSAtomicCompareAndSwapPtrBarrier(nil, worker, (void * volatile *)&descriptor)) {
+ [worker release];
+ }
+ }
+ return descriptor;
+}
+
+BOOL GtalkRosterItem_DisplayType_IsValidValue(int32_t value__) {
+ switch (value__) {
+ case GtalkRosterItem_DisplayType_Blocked:
+ case GtalkRosterItem_DisplayType_Hidden:
+ case GtalkRosterItem_DisplayType_Pinned:
+ return YES;
+ default:
+ return NO;
+ }
+}
+
+#pragma mark - GtalkRmqLastId
+
+@implementation GtalkRmqLastId
+
+@dynamic hasId_p, id_p;
+
+typedef struct GtalkRmqLastId__storage_ {
+ uint32_t _has_storage_[1];
+ int64_t id_p;
+} GtalkRmqLastId__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "id_p",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkRmqLastId_FieldNumber_Id_p,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(GtalkRmqLastId__storage_, id_p),
+ .flags = GPBFieldRequired,
+ .dataType = GPBDataTypeInt64,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GtalkRmqLastId class]
+ rootClass:[GtalkGtalkExtensionsRoot class]
+ file:GtalkGtalkExtensionsRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GtalkRmqLastId__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+#pragma mark - GtalkRmqAck
+
+@implementation GtalkRmqAck
+
+@dynamic hasId_p, id_p;
+
+typedef struct GtalkRmqAck__storage_ {
+ uint32_t _has_storage_[1];
+ int64_t id_p;
+} GtalkRmqAck__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "id_p",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkRmqAck_FieldNumber_Id_p,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(GtalkRmqAck__storage_, id_p),
+ .flags = GPBFieldRequired,
+ .dataType = GPBDataTypeInt64,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GtalkRmqAck class]
+ rootClass:[GtalkGtalkExtensionsRoot class]
+ file:GtalkGtalkExtensionsRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GtalkRmqAck__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+#pragma mark - GtalkVCard
+
+@implementation GtalkVCard
+
+@dynamic hasVersion, version;
+@dynamic hasFullName, fullName;
+@dynamic hasPhoto, photo;
+@dynamic hasAvatarHash, avatarHash;
+@dynamic hasModified, modified;
+
+typedef struct GtalkVCard__storage_ {
+ uint32_t _has_storage_[1];
+ NSString *version;
+ NSString *fullName;
+ GtalkPhoto *photo;
+ NSString *avatarHash;
+} GtalkVCard__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "version",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkVCard_FieldNumber_Version,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(GtalkVCard__storage_, version),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "fullName",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkVCard_FieldNumber_FullName,
+ .hasIndex = 1,
+ .offset = (uint32_t)offsetof(GtalkVCard__storage_, fullName),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "photo",
+ .dataTypeSpecific.className = GPBStringifySymbol(GtalkPhoto),
+ .number = GtalkVCard_FieldNumber_Photo,
+ .hasIndex = 2,
+ .offset = (uint32_t)offsetof(GtalkVCard__storage_, photo),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeMessage,
+ },
+ {
+ .name = "avatarHash",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkVCard_FieldNumber_AvatarHash,
+ .hasIndex = 3,
+ .offset = (uint32_t)offsetof(GtalkVCard__storage_, avatarHash),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "modified",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkVCard_FieldNumber_Modified,
+ .hasIndex = 4,
+ .offset = 5, // Stored in _has_storage_ to save space.
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeBool,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GtalkVCard class]
+ rootClass:[GtalkGtalkExtensionsRoot class]
+ file:GtalkGtalkExtensionsRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GtalkVCard__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+#pragma mark - GtalkPhoto
+
+@implementation GtalkPhoto
+
+@dynamic hasType, type;
+@dynamic hasData_p, data_p;
+
+typedef struct GtalkPhoto__storage_ {
+ uint32_t _has_storage_[1];
+ NSString *type;
+ NSString *data_p;
+} GtalkPhoto__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "type",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkPhoto_FieldNumber_Type,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(GtalkPhoto__storage_, type),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "data_p",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkPhoto_FieldNumber_Data_p,
+ .hasIndex = 1,
+ .offset = (uint32_t)offsetof(GtalkPhoto__storage_, data_p),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GtalkPhoto class]
+ rootClass:[GtalkGtalkExtensionsRoot class]
+ file:GtalkGtalkExtensionsRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GtalkPhoto__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+#pragma mark - GtalkChatRead
+
+@implementation GtalkChatRead
+
+@dynamic hasUser, user;
+
+typedef struct GtalkChatRead__storage_ {
+ uint32_t _has_storage_[1];
+ NSString *user;
+} GtalkChatRead__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "user",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkChatRead_FieldNumber_User,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(GtalkChatRead__storage_, user),
+ .flags = GPBFieldRequired,
+ .dataType = GPBDataTypeString,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GtalkChatRead class]
+ rootClass:[GtalkGtalkExtensionsRoot class]
+ file:GtalkGtalkExtensionsRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GtalkChatRead__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+#pragma mark - GtalkChatClosed
+
+@implementation GtalkChatClosed
+
+@dynamic hasUser, user;
+
+typedef struct GtalkChatClosed__storage_ {
+ uint32_t _has_storage_[1];
+ NSString *user;
+} GtalkChatClosed__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "user",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkChatClosed_FieldNumber_User,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(GtalkChatClosed__storage_, user),
+ .flags = GPBFieldRequired,
+ .dataType = GPBDataTypeString,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GtalkChatClosed class]
+ rootClass:[GtalkGtalkExtensionsRoot class]
+ file:GtalkGtalkExtensionsRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GtalkChatClosed__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+#pragma mark - GtalkCapabilities
+
+@implementation GtalkCapabilities
+
+@dynamic hasNode, node;
+@dynamic hasVer, ver;
+@dynamic hasExt, ext;
+@dynamic hasHash_p, hash_p;
+
+typedef struct GtalkCapabilities__storage_ {
+ uint32_t _has_storage_[1];
+ NSString *node;
+ NSString *ver;
+ NSString *ext;
+ NSString *hash_p;
+} GtalkCapabilities__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "node",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkCapabilities_FieldNumber_Node,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(GtalkCapabilities__storage_, node),
+ .flags = GPBFieldRequired,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "ver",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkCapabilities_FieldNumber_Ver,
+ .hasIndex = 1,
+ .offset = (uint32_t)offsetof(GtalkCapabilities__storage_, ver),
+ .flags = GPBFieldRequired,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "ext",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkCapabilities_FieldNumber_Ext,
+ .hasIndex = 2,
+ .offset = (uint32_t)offsetof(GtalkCapabilities__storage_, ext),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "hash_p",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkCapabilities_FieldNumber_Hash_p,
+ .hasIndex = 3,
+ .offset = (uint32_t)offsetof(GtalkCapabilities__storage_, hash_p),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GtalkCapabilities class]
+ rootClass:[GtalkGtalkExtensionsRoot class]
+ file:GtalkGtalkExtensionsRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GtalkCapabilities__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+#pragma mark - GtalkSharedStatus
+
+@implementation GtalkSharedStatus
+
+@dynamic hasStatusMax, statusMax;
+@dynamic hasStatusListMax, statusListMax;
+@dynamic hasStatusListContentsMax, statusListContentsMax;
+@dynamic hasStatus, status;
+@dynamic hasShow, show;
+@dynamic statusListArray, statusListArray_Count;
+@dynamic hasInvisible, invisible;
+@dynamic hasStatusMinVersion, statusMinVersion;
+
+typedef struct GtalkSharedStatus__storage_ {
+ uint32_t _has_storage_[1];
+ int32_t statusMax;
+ int32_t statusListMax;
+ int32_t statusListContentsMax;
+ GtalkSharedStatus_ShowType show;
+ int32_t statusMinVersion;
+ NSString *status;
+ NSMutableArray *statusListArray;
+} GtalkSharedStatus__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "statusMax",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkSharedStatus_FieldNumber_StatusMax,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(GtalkSharedStatus__storage_, statusMax),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt32,
+ },
+ {
+ .name = "statusListMax",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkSharedStatus_FieldNumber_StatusListMax,
+ .hasIndex = 1,
+ .offset = (uint32_t)offsetof(GtalkSharedStatus__storage_, statusListMax),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt32,
+ },
+ {
+ .name = "statusListContentsMax",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkSharedStatus_FieldNumber_StatusListContentsMax,
+ .hasIndex = 2,
+ .offset = (uint32_t)offsetof(GtalkSharedStatus__storage_, statusListContentsMax),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt32,
+ },
+ {
+ .name = "status",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkSharedStatus_FieldNumber_Status,
+ .hasIndex = 3,
+ .offset = (uint32_t)offsetof(GtalkSharedStatus__storage_, status),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "show",
+ .dataTypeSpecific.enumDescFunc = GtalkSharedStatus_ShowType_EnumDescriptor,
+ .number = GtalkSharedStatus_FieldNumber_Show,
+ .hasIndex = 4,
+ .offset = (uint32_t)offsetof(GtalkSharedStatus__storage_, show),
+ .flags = (GPBFieldFlags)(GPBFieldOptional | GPBFieldHasEnumDescriptor),
+ .dataType = GPBDataTypeEnum,
+ },
+ {
+ .name = "statusListArray",
+ .dataTypeSpecific.className = GPBStringifySymbol(GtalkSharedStatus_StatusList),
+ .number = GtalkSharedStatus_FieldNumber_StatusListArray,
+ .hasIndex = GPBNoHasBit,
+ .offset = (uint32_t)offsetof(GtalkSharedStatus__storage_, statusListArray),
+ .flags = GPBFieldRepeated,
+ .dataType = GPBDataTypeGroup,
+ },
+ {
+ .name = "invisible",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkSharedStatus_FieldNumber_Invisible,
+ .hasIndex = 5,
+ .offset = 6, // Stored in _has_storage_ to save space.
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeBool,
+ },
+ {
+ .name = "statusMinVersion",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkSharedStatus_FieldNumber_StatusMinVersion,
+ .hasIndex = 7,
+ .offset = (uint32_t)offsetof(GtalkSharedStatus__storage_, statusMinVersion),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt32,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GtalkSharedStatus class]
+ rootClass:[GtalkGtalkExtensionsRoot class]
+ file:GtalkGtalkExtensionsRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GtalkSharedStatus__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+#pragma mark - Enum GtalkSharedStatus_ShowType
+
+GPBEnumDescriptor *GtalkSharedStatus_ShowType_EnumDescriptor(void) {
+ static GPBEnumDescriptor *descriptor = NULL;
+ if (!descriptor) {
+ static const char *valueNames =
+ "Default\000Dnd\000";
+ static const int32_t values[] = {
+ GtalkSharedStatus_ShowType_Default,
+ GtalkSharedStatus_ShowType_Dnd,
+ };
+ GPBEnumDescriptor *worker =
+ [GPBEnumDescriptor allocDescriptorForName:GPBNSStringifySymbol(GtalkSharedStatus_ShowType)
+ valueNames:valueNames
+ values:values
+ count:(uint32_t)(sizeof(values) / sizeof(int32_t))
+ enumVerifier:GtalkSharedStatus_ShowType_IsValidValue];
+ if (!OSAtomicCompareAndSwapPtrBarrier(nil, worker, (void * volatile *)&descriptor)) {
+ [worker release];
+ }
+ }
+ return descriptor;
+}
+
+BOOL GtalkSharedStatus_ShowType_IsValidValue(int32_t value__) {
+ switch (value__) {
+ case GtalkSharedStatus_ShowType_Default:
+ case GtalkSharedStatus_ShowType_Dnd:
+ return YES;
+ default:
+ return NO;
+ }
+}
+
+#pragma mark - GtalkSharedStatus_StatusList
+
+@implementation GtalkSharedStatus_StatusList
+
+@dynamic hasShow, show;
+@dynamic statusArray, statusArray_Count;
+
+typedef struct GtalkSharedStatus_StatusList__storage_ {
+ uint32_t _has_storage_[1];
+ GtalkSharedStatus_ShowType show;
+ NSMutableArray *statusArray;
+} GtalkSharedStatus_StatusList__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "show",
+ .dataTypeSpecific.enumDescFunc = GtalkSharedStatus_ShowType_EnumDescriptor,
+ .number = GtalkSharedStatus_StatusList_FieldNumber_Show,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(GtalkSharedStatus_StatusList__storage_, show),
+ .flags = (GPBFieldFlags)(GPBFieldRequired | GPBFieldHasEnumDescriptor),
+ .dataType = GPBDataTypeEnum,
+ },
+ {
+ .name = "statusArray",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkSharedStatus_StatusList_FieldNumber_StatusArray,
+ .hasIndex = GPBNoHasBit,
+ .offset = (uint32_t)offsetof(GtalkSharedStatus_StatusList__storage_, statusArray),
+ .flags = GPBFieldRepeated,
+ .dataType = GPBDataTypeString,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GtalkSharedStatus_StatusList class]
+ rootClass:[GtalkGtalkExtensionsRoot class]
+ file:GtalkGtalkExtensionsRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GtalkSharedStatus_StatusList__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ [localDescriptor setupContainingMessageClassName:GPBStringifySymbol(GtalkSharedStatus)];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+#pragma mark - GtalkOtrQuery
+
+@implementation GtalkOtrQuery
+
+@dynamic hasNosaveDefault, nosaveDefault;
+@dynamic itemArray, itemArray_Count;
+@dynamic hasEtag, etag;
+@dynamic hasNotModified, notModified;
+
+typedef struct GtalkOtrQuery__storage_ {
+ uint32_t _has_storage_[1];
+ NSMutableArray *itemArray;
+ NSString *etag;
+} GtalkOtrQuery__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "nosaveDefault",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkOtrQuery_FieldNumber_NosaveDefault,
+ .hasIndex = 0,
+ .offset = 1, // Stored in _has_storage_ to save space.
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeBool,
+ },
+ {
+ .name = "itemArray",
+ .dataTypeSpecific.className = GPBStringifySymbol(GtalkOtrItem),
+ .number = GtalkOtrQuery_FieldNumber_ItemArray,
+ .hasIndex = GPBNoHasBit,
+ .offset = (uint32_t)offsetof(GtalkOtrQuery__storage_, itemArray),
+ .flags = GPBFieldRepeated,
+ .dataType = GPBDataTypeMessage,
+ },
+ {
+ .name = "etag",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkOtrQuery_FieldNumber_Etag,
+ .hasIndex = 2,
+ .offset = (uint32_t)offsetof(GtalkOtrQuery__storage_, etag),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "notModified",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkOtrQuery_FieldNumber_NotModified,
+ .hasIndex = 3,
+ .offset = 4, // Stored in _has_storage_ to save space.
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeBool,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GtalkOtrQuery class]
+ rootClass:[GtalkGtalkExtensionsRoot class]
+ file:GtalkGtalkExtensionsRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GtalkOtrQuery__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+#pragma mark - GtalkOtrItem
+
+@implementation GtalkOtrItem
+
+@dynamic hasJid, jid;
+@dynamic hasNosave, nosave;
+@dynamic hasChangedByBuddy, changedByBuddy;
+
+typedef struct GtalkOtrItem__storage_ {
+ uint32_t _has_storage_[1];
+ NSString *jid;
+} GtalkOtrItem__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "jid",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkOtrItem_FieldNumber_Jid,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(GtalkOtrItem__storage_, jid),
+ .flags = GPBFieldRequired,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "nosave",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkOtrItem_FieldNumber_Nosave,
+ .hasIndex = 1,
+ .offset = 2, // Stored in _has_storage_ to save space.
+ .flags = GPBFieldRequired,
+ .dataType = GPBDataTypeBool,
+ },
+ {
+ .name = "changedByBuddy",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkOtrItem_FieldNumber_ChangedByBuddy,
+ .hasIndex = 3,
+ .offset = 4, // Stored in _has_storage_ to save space.
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeBool,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GtalkOtrItem class]
+ rootClass:[GtalkGtalkExtensionsRoot class]
+ file:GtalkGtalkExtensionsRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GtalkOtrItem__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+#pragma mark - GtalkIdle
+
+@implementation GtalkIdle
+
+@dynamic hasIdle, idle;
+@dynamic hasAway, away;
+
+typedef struct GtalkIdle__storage_ {
+ uint32_t _has_storage_[1];
+} GtalkIdle__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "idle",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkIdle_FieldNumber_Idle,
+ .hasIndex = 0,
+ .offset = 1, // Stored in _has_storage_ to save space.
+ .flags = GPBFieldRequired,
+ .dataType = GPBDataTypeBool,
+ },
+ {
+ .name = "away",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkIdle_FieldNumber_Away,
+ .hasIndex = 2,
+ .offset = 3, // Stored in _has_storage_ to save space.
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeBool,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GtalkIdle class]
+ rootClass:[GtalkGtalkExtensionsRoot class]
+ file:GtalkGtalkExtensionsRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GtalkIdle__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+#pragma mark - GtalkPostAuthBatchQuery
+
+@implementation GtalkPostAuthBatchQuery
+
+@dynamic hasAvailable, available;
+@dynamic hasDeviceIdle, deviceIdle;
+@dynamic hasMobileIndicator, mobileIndicator;
+@dynamic hasSharedStatusVersion, sharedStatusVersion;
+@dynamic hasRosterEtag, rosterEtag;
+@dynamic hasOtrEtag, otrEtag;
+@dynamic hasAvatarHash, avatarHash;
+@dynamic hasVcardQueryStanzaId, vcardQueryStanzaId;
+@dynamic hasCapabilitiesExtFlags, capabilitiesExtFlags;
+
+typedef struct GtalkPostAuthBatchQuery__storage_ {
+ uint32_t _has_storage_[1];
+ int32_t sharedStatusVersion;
+ int32_t capabilitiesExtFlags;
+ NSString *rosterEtag;
+ NSString *otrEtag;
+ NSString *avatarHash;
+ NSString *vcardQueryStanzaId;
+} GtalkPostAuthBatchQuery__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "available",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkPostAuthBatchQuery_FieldNumber_Available,
+ .hasIndex = 0,
+ .offset = 1, // Stored in _has_storage_ to save space.
+ .flags = GPBFieldRequired,
+ .dataType = GPBDataTypeBool,
+ },
+ {
+ .name = "deviceIdle",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkPostAuthBatchQuery_FieldNumber_DeviceIdle,
+ .hasIndex = 2,
+ .offset = 3, // Stored in _has_storage_ to save space.
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeBool,
+ },
+ {
+ .name = "mobileIndicator",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkPostAuthBatchQuery_FieldNumber_MobileIndicator,
+ .hasIndex = 4,
+ .offset = 5, // Stored in _has_storage_ to save space.
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeBool,
+ },
+ {
+ .name = "sharedStatusVersion",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkPostAuthBatchQuery_FieldNumber_SharedStatusVersion,
+ .hasIndex = 6,
+ .offset = (uint32_t)offsetof(GtalkPostAuthBatchQuery__storage_, sharedStatusVersion),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt32,
+ },
+ {
+ .name = "rosterEtag",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkPostAuthBatchQuery_FieldNumber_RosterEtag,
+ .hasIndex = 7,
+ .offset = (uint32_t)offsetof(GtalkPostAuthBatchQuery__storage_, rosterEtag),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "otrEtag",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkPostAuthBatchQuery_FieldNumber_OtrEtag,
+ .hasIndex = 8,
+ .offset = (uint32_t)offsetof(GtalkPostAuthBatchQuery__storage_, otrEtag),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "avatarHash",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkPostAuthBatchQuery_FieldNumber_AvatarHash,
+ .hasIndex = 9,
+ .offset = (uint32_t)offsetof(GtalkPostAuthBatchQuery__storage_, avatarHash),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "vcardQueryStanzaId",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkPostAuthBatchQuery_FieldNumber_VcardQueryStanzaId,
+ .hasIndex = 10,
+ .offset = (uint32_t)offsetof(GtalkPostAuthBatchQuery__storage_, vcardQueryStanzaId),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "capabilitiesExtFlags",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkPostAuthBatchQuery_FieldNumber_CapabilitiesExtFlags,
+ .hasIndex = 11,
+ .offset = (uint32_t)offsetof(GtalkPostAuthBatchQuery__storage_, capabilitiesExtFlags),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt32,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GtalkPostAuthBatchQuery class]
+ rootClass:[GtalkGtalkExtensionsRoot class]
+ file:GtalkGtalkExtensionsRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GtalkPostAuthBatchQuery__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+#pragma mark - Enum GtalkPostAuthBatchQuery_CapabilitiesExtFlags
+
+GPBEnumDescriptor *GtalkPostAuthBatchQuery_CapabilitiesExtFlags_EnumDescriptor(void) {
+ static GPBEnumDescriptor *descriptor = NULL;
+ if (!descriptor) {
+ static const char *valueNames =
+ "HasVoiceV1\000HasVideoV1\000HasCameraV1\000HasPmu"
+ "cV1\000";
+ static const int32_t values[] = {
+ GtalkPostAuthBatchQuery_CapabilitiesExtFlags_HasVoiceV1,
+ GtalkPostAuthBatchQuery_CapabilitiesExtFlags_HasVideoV1,
+ GtalkPostAuthBatchQuery_CapabilitiesExtFlags_HasCameraV1,
+ GtalkPostAuthBatchQuery_CapabilitiesExtFlags_HasPmucV1,
+ };
+ GPBEnumDescriptor *worker =
+ [GPBEnumDescriptor allocDescriptorForName:GPBNSStringifySymbol(GtalkPostAuthBatchQuery_CapabilitiesExtFlags)
+ valueNames:valueNames
+ values:values
+ count:(uint32_t)(sizeof(values) / sizeof(int32_t))
+ enumVerifier:GtalkPostAuthBatchQuery_CapabilitiesExtFlags_IsValidValue];
+ if (!OSAtomicCompareAndSwapPtrBarrier(nil, worker, (void * volatile *)&descriptor)) {
+ [worker release];
+ }
+ }
+ return descriptor;
+}
+
+BOOL GtalkPostAuthBatchQuery_CapabilitiesExtFlags_IsValidValue(int32_t value__) {
+ switch (value__) {
+ case GtalkPostAuthBatchQuery_CapabilitiesExtFlags_HasVoiceV1:
+ case GtalkPostAuthBatchQuery_CapabilitiesExtFlags_HasVideoV1:
+ case GtalkPostAuthBatchQuery_CapabilitiesExtFlags_HasCameraV1:
+ case GtalkPostAuthBatchQuery_CapabilitiesExtFlags_HasPmucV1:
+ return YES;
+ default:
+ return NO;
+ }
+}
+
+#pragma mark - GtalkStreamAck
+
+@implementation GtalkStreamAck
+
+
+typedef struct GtalkStreamAck__storage_ {
+ uint32_t _has_storage_[1];
+} GtalkStreamAck__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GtalkStreamAck class]
+ rootClass:[GtalkGtalkExtensionsRoot class]
+ file:GtalkGtalkExtensionsRoot_FileDescriptor()
+ fields:NULL
+ fieldCount:0
+ storageSize:sizeof(GtalkStreamAck__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+#pragma mark - GtalkSelectiveAck
+
+@implementation GtalkSelectiveAck
+
+@dynamic idArray, idArray_Count;
+
+typedef struct GtalkSelectiveAck__storage_ {
+ uint32_t _has_storage_[1];
+ NSMutableArray *idArray;
+} GtalkSelectiveAck__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "idArray",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkSelectiveAck_FieldNumber_IdArray,
+ .hasIndex = GPBNoHasBit,
+ .offset = (uint32_t)offsetof(GtalkSelectiveAck__storage_, idArray),
+ .flags = GPBFieldRepeated,
+ .dataType = GPBDataTypeString,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GtalkSelectiveAck class]
+ rootClass:[GtalkGtalkExtensionsRoot class]
+ file:GtalkGtalkExtensionsRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GtalkSelectiveAck__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+
+#pragma clang diagnostic pop
+
+// @@protoc_insertion_point(global_scope)
diff --git a/Firebase/Messaging/Public/FIRMessaging.h b/Firebase/Messaging/Public/FIRMessaging.h
new file mode 100644
index 0000000..84d2526
--- /dev/null
+++ b/Firebase/Messaging/Public/FIRMessaging.h
@@ -0,0 +1,486 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+
+// NS_SWIFT_NAME can only translate factory methods before the iOS 9.3 SDK.
+// Wrap it in our own macro if it's a non-compatible SDK.
+#ifndef FIR_SWIFT_NAME
+#ifdef __IPHONE_9_3
+#define FIR_SWIFT_NAME(X) NS_SWIFT_NAME(X)
+#else
+#define FIR_SWIFT_NAME(X) // Intentionally blank.
+#endif // #ifdef __IPHONE_9_3
+#endif // #ifndef FIR_SWIFT_NAME
+
+/**
+ * @related FIRMessaging
+ *
+ * The completion handler invoked when the registration token returns.
+ * If the call fails we return the appropriate `error code`, described by
+ * `FIRMessagingError`.
+ *
+ * @param FCMToken The valid registration token returned by FCM.
+ * @param error The error describing why a token request failed. The error code
+ * will match a value from the FIRMessagingError enumeration.
+ */
+typedef void(^FIRMessagingFCMTokenFetchCompletion)(NSString * _Nullable FCMToken,
+ NSError * _Nullable error)
+ FIR_SWIFT_NAME(MessagingFCMTokenFetchCompletion);
+
+
+/**
+ * @related FIRMessaging
+ *
+ * The completion handler invoked when the registration token deletion request is
+ * completed. If the call fails we return the appropriate `error code`, described
+ * by `FIRMessagingError`.
+ *
+ * @param error The error describing why a token deletion failed. The error code
+ * will match a value from the FIRMessagingError enumeration.
+ */
+typedef void(^FIRMessagingDeleteFCMTokenCompletion)(NSError * _Nullable error)
+ FIR_SWIFT_NAME(MessagingDeleteFCMTokenCompletion);
+
+/**
+ * The completion handler invoked once the data connection with FIRMessaging is
+ * established. The data connection is used to send a continous stream of
+ * data and all the FIRMessaging data notifications arrive through this connection.
+ * Once the connection is established we invoke the callback with `nil` error.
+ * Correspondingly if we get an error while trying to establish a connection
+ * we invoke the handler with an appropriate error object and do an
+ * exponential backoff to try and connect again unless successful.
+ *
+ * @param error The error object if any describing why the data connection
+ * to FIRMessaging failed.
+ */
+typedef void(^FIRMessagingConnectCompletion)(NSError * __nullable error)
+ FIR_SWIFT_NAME(MessagingConnectCompletion)
+ __deprecated_msg("Please listen for the FIRMessagingConnectionStateChangedNotification "
+ "NSNotification instead.");
+
+#if defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0
+/**
+ * Notification sent when the upstream message has been delivered
+ * successfully to the server. The notification object will be the messageID
+ * of the successfully delivered message.
+ */
+FOUNDATION_EXPORT const NSNotificationName __nonnull FIRMessagingSendSuccessNotification
+ FIR_SWIFT_NAME(MessagingSendSuccess);
+
+/**
+ * Notification sent when the upstream message was failed to be sent to the
+ * server. The notification object will be the messageID of the failed
+ * message. The userInfo dictionary will contain the relevant error
+ * information for the failure.
+ */
+FOUNDATION_EXPORT const NSNotificationName __nonnull FIRMessagingSendErrorNotification
+ FIR_SWIFT_NAME(MessagingSendError);
+
+/**
+ * Notification sent when the Firebase messaging server deletes pending
+ * messages due to exceeded storage limits. This may occur, for example, when
+ * the device cannot be reached for an extended period of time.
+ *
+ * It is recommended to retrieve any missing messages directly from the
+ * server.
+ */
+FOUNDATION_EXPORT const NSNotificationName __nonnull FIRMessagingMessagesDeletedNotification
+ FIR_SWIFT_NAME(MessagingMessagesDeleted);
+
+/**
+ * Notification sent when Firebase Messaging establishes or disconnects from
+ * an FCM socket connection. You can query the connection state in this
+ * notification by checking the `isDirectChannelEstablished` property of FIRMessaging.
+ */
+FOUNDATION_EXPORT const NSNotificationName __nonnull FIRMessagingConnectionStateChangedNotification
+ FIR_SWIFT_NAME(MessagingConnectionStateChanged);
+
+/**
+ * Notification sent when the FCM registration token has been refreshed. You can also
+ * receive the FCM token via the FIRMessagingDelegate method
+ * `-messaging:didRefreshRegistrationToken:`
+ */
+FOUNDATION_EXPORT const NSNotificationName __nonnull
+ FIRMessagingRegistrationTokenRefreshedNotification
+ FIR_SWIFT_NAME(MessagingRegistrationTokenRefreshed);
+#else
+/**
+ * Notification sent when the upstream message has been delivered
+ * successfully to the server. The notification object will be the messageID
+ * of the successfully delivered message.
+ */
+FOUNDATION_EXPORT NSString * __nonnull const FIRMessagingSendSuccessNotification
+ FIR_SWIFT_NAME(MessagingSendSuccessNotification);
+
+/**
+ * Notification sent when the upstream message was failed to be sent to the
+ * server. The notification object will be the messageID of the failed
+ * message. The userInfo dictionary will contain the relevant error
+ * information for the failure.
+ */
+FOUNDATION_EXPORT NSString * __nonnull const FIRMessagingSendErrorNotification
+ FIR_SWIFT_NAME(MessagingSendErrorNotification);
+
+/**
+ * Notification sent when the Firebase messaging server deletes pending
+ * messages due to exceeded storage limits. This may occur, for example, when
+ * the device cannot be reached for an extended period of time.
+ *
+ * It is recommended to retrieve any missing messages directly from the
+ * server.
+ */
+FOUNDATION_EXPORT NSString * __nonnull const FIRMessagingMessagesDeletedNotification
+ FIR_SWIFT_NAME(MessagingMessagesDeletedNotification);
+
+/**
+ * Notification sent when Firebase Messaging establishes or disconnects from
+ * an FCM socket connection. You can query the connection state in this
+ * notification by checking the `isDirectChannelEstablished` property of FIRMessaging.
+ */
+FOUNDATION_EXPORT NSString * __nonnull const FIRMessagingConnectionStateChangedNotification
+ FIR_SWIFT_NAME(MessagingConnectionStateChangedNotification);
+
+/**
+ * Notification sent when the FCM registration token has been refreshed. You can also
+ * receive the FCM token via the FIRMessagingDelegate method
+ * `-messaging:didRefreshRegistrationToken:`
+ */
+FOUNDATION_EXPORT NSString * __nonnull const FIRMessagingRegistrationTokenRefreshedNotification
+ FIR_SWIFT_NAME(MessagingRegistrationTokenRefreshedNotification);
+#endif // defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0
+
+/**
+ * @enum FIRMessagingError
+ */
+typedef NS_ENUM(NSUInteger, FIRMessagingError) {
+ /// Unknown error.
+ FIRMessagingErrorUnknown = 0,
+
+ /// FIRMessaging couldn't validate request from this client.
+ FIRMessagingErrorAuthentication = 1,
+
+ /// InstanceID service cannot be accessed.
+ FIRMessagingErrorNoAccess = 2,
+
+ /// Request to InstanceID backend timed out.
+ FIRMessagingErrorTimeout = 3,
+
+ /// No network available to reach the servers.
+ FIRMessagingErrorNetwork = 4,
+
+ /// Another similar operation in progress, bailing this one.
+ FIRMessagingErrorOperationInProgress = 5,
+
+ /// Some parameters of the request were invalid.
+ FIRMessagingErrorInvalidRequest = 7,
+} FIR_SWIFT_NAME(MessagingError);
+
+/// Status for the downstream message received by the app.
+typedef NS_ENUM(NSInteger, FIRMessagingMessageStatus) {
+ /// Unknown status.
+ FIRMessagingMessageStatusUnknown,
+ /// New downstream message received by the app.
+ FIRMessagingMessageStatusNew,
+} FIR_SWIFT_NAME(MessagingMessageStatus);
+
+/**
+ * The APNS token type for the app. If the token type is set to `UNKNOWN`
+ * Firebase Messaging will implicitly try to figure out what the actual token type
+ * is from the provisioning profile.
+ * Unless you really need to specify the type, you should use the `APNSToken`
+ * property instead.
+ */
+typedef NS_ENUM(NSInteger, FIRMessagingAPNSTokenType) {
+ /// Unknown token type.
+ FIRMessagingAPNSTokenTypeUnknown,
+ /// Sandbox token type.
+ FIRMessagingAPNSTokenTypeSandbox,
+ /// Production token type.
+ FIRMessagingAPNSTokenTypeProd,
+} FIR_SWIFT_NAME(MessagingAPNSTokenType);
+
+/// Information about a downstream message received by the app.
+FIR_SWIFT_NAME(MessagingMessageInfo)
+@interface FIRMessagingMessageInfo : NSObject
+
+/// The status of the downstream message
+@property(nonatomic, readonly, assign) FIRMessagingMessageStatus status;
+
+@end
+
+/**
+ * A remote data message received by the app via FCM (not just the APNs interface).
+ *
+ * This is only for devices running iOS 10 or above. To support devices running iOS 9 or below, use
+ * the local and remote notifications handlers defined in UIApplicationDelegate protocol.
+ */
+FIR_SWIFT_NAME(MessagingRemoteMessage)
+@interface FIRMessagingRemoteMessage : NSObject
+
+/// The downstream message received by the application.
+@property(nonatomic, readonly, strong, nonnull) NSDictionary *appData;
+@end
+
+@class FIRMessaging;
+/**
+ * A protocol to handle events from FCM for devices running iOS 10 or above.
+ *
+ * To support devices running iOS 9 or below, use the local and remote notifications handlers
+ * defined in UIApplicationDelegate protocol.
+ */
+FIR_SWIFT_NAME(MessagingDelegate)
+@protocol FIRMessagingDelegate <NSObject>
+
+/// This method will be called whenever FCM receives a new, default FCM token for your
+/// Firebase project's Sender ID.
+/// You can send this token to your application server to send notifications to this device.
+- (void)messaging:(nonnull FIRMessaging *)messaging
+ didRefreshRegistrationToken:(nonnull NSString *)fcmToken
+ FIR_SWIFT_NAME(messaging(_:didRefreshRegistrationToken:));
+
+@optional
+/// This method is called on iOS 10 devices to handle data messages received via FCM through its
+/// direct channel (not via APNS). For iOS 9 and below, the FCM data message is delivered via the
+/// UIApplicationDelegate's -application:didReceiveRemoteNotification: method.
+- (void)messaging:(nonnull FIRMessaging *)messaging
+ didReceiveMessage:(nonnull FIRMessagingRemoteMessage *)remoteMessage
+ FIR_SWIFT_NAME(messaging(_:didReceive:))
+ __IOS_AVAILABLE(10.0);
+
+/// The callback to handle data message received via FCM for devices running iOS 10 or above.
+- (void)applicationReceivedRemoteMessage:(nonnull FIRMessagingRemoteMessage *)remoteMessage
+ FIR_SWIFT_NAME(application(received:))
+ __deprecated_msg("Use FIRMessagingDelegate’s -messaging:didReceiveMessage:");
+
+@end
+
+/**
+ * Firebase Messaging lets you reliably deliver messages at no cost.
+ *
+ * To send or receive messages, the app must get a
+ * registration token from FIRInstanceID. This token authorizes an
+ * app server to send messages to an app instance.
+ *
+ * In order to receive FIRMessaging messages, declare `application:didReceiveRemoteNotification:`.
+ */
+FIR_SWIFT_NAME(Messaging)
+@interface FIRMessaging : NSObject
+
+/**
+ * Delegate to handle FCM token refreshes, and remote data messages received via FCM for devices
+ * running iOS 10 or above.
+ */
+@property(nonatomic, weak, nullable) id<FIRMessagingDelegate> delegate;
+
+
+/**
+ * Delegate to handle remote data messages received via FCM for devices running iOS 10 or above.
+ */
+@property(nonatomic, weak, nullable) id<FIRMessagingDelegate> remoteMessageDelegate
+ __deprecated_msg("Use 'delegate' property");
+
+/**
+ * When set to YES, Firebase Messaging will automatically establish a socket-based, direct channel
+ * to the FCM server. You only need to enable this if you are sending upstream messages or
+ * receiving non-APNS, data-only messages in foregrounded apps.
+ * Default is NO.
+ */
+@property(nonatomic) BOOL shouldEstablishDirectChannel;
+
+/**
+ * Returns YES if the direct channel to the FCM server is active, NO otherwise.
+ */
+@property(nonatomic, readonly) BOOL isDirectChannelEstablished;
+
+/**
+ * FIRMessaging
+ *
+ * @return An instance of FIRMessaging.
+ */
++ (nonnull instancetype)messaging FIR_SWIFT_NAME(messaging());
+
+/**
+ * Unavailable. Use +messaging instead.
+ */
+- (nonnull instancetype)init __attribute__((unavailable("Use +messaging instead.")));
+
+#pragma mark - APNS
+
+/**
+ * This property is used to set the APNS Token received by the application delegate.
+ *
+ * FIRMessaging uses method swizzling to ensure the APNS token is set automatically.
+ * However, if you have disabled swizzling by setting `FirebaseAppDelegateProxyEnabled`
+ * to `NO` in your app's Info.plist, you should manually set the APNS token in your
+ * application delegate's -application:didRegisterForRemoteNotificationsWithDeviceToken:
+ * method.
+ *
+ * If you would like to set the type of the APNS token, rather than relying on automatic
+ * detection, see: -setAPNSToken:type:.
+ */
+@property(nonatomic, copy, nullable) NSData *APNSToken FIR_SWIFT_NAME(apnsToken);
+
+/**
+ * Set APNS token for the application. This APNS token will be used to register
+ * with Firebase Messaging using `FCMToken` or
+ * `tokenWithAuthorizedEntity:scope:options:handler`.
+ *
+ * @param apnsToken The APNS token for the application.
+ * @param type The type of APNS token. Debug builds should use
+ * FIRMessagingAPNSTokenTypeSandbox. Alternatively, you can supply
+ * FIRMessagingAPNSTokenTypeUnknown to have the type automatically
+ * detected based on your provisioning profile.
+ */
+- (void)setAPNSToken:(nonnull NSData *)apnsToken type:(FIRMessagingAPNSTokenType)type;
+
+#pragma mark - FCM Tokens
+
+/**
+ * The FCM token is used to identify this device so that FCM can send notifications to it.
+ * It is associated with your APNS token when the APNS token is supplied, so that sending
+ * messages to the FCM token will be delivered over APNS.
+ *
+ * The FCM token is sometimes refreshed automatically. You can be notified of these changes
+ * via the FIRMessagingDelegate method `-message:didRefreshRegistrationToken:`, or by
+ * listening for the `FIRMessagingRegistrationTokenRefreshedNotification` notification.
+ *
+ * Once you have an FCM token, you should send it to your application server, so it can use
+ * the FCM token to send notifications to your device.
+ */
+@property(nonatomic, readonly, nullable) NSString *FCMToken FIR_SWIFT_NAME(fcmToken);
+
+
+/**
+ * Retrieves an FCM registration token for a particular Sender ID. This registration token is
+ * not cached by FIRMessaging. FIRMessaging should have an APNS token set before calling this
+ * to ensure that notifications can be delivered via APNS using this FCM token. You may
+ * re-retrieve the FCM token once you have the APNS token set, to associate it with the FCM
+ * token. The default FCM token is automatically associated with the APNS token, if the APNS
+ * token data is available.
+ *
+ * @param senderID The Sender ID for a particular Firebase project.
+ * @param completion The completion handler to handle the token request.
+ */
+- (void)retrieveFCMTokenForSenderID:(nonnull NSString *)senderID
+ completion:(nonnull FIRMessagingFCMTokenFetchCompletion)completion
+ FIR_SWIFT_NAME(retrieveFCMToken(forSenderID:completion:));
+
+
+/**
+ * Invalidates an FCM token for a particular Sender ID. That Sender ID cannot no longer send
+ * notifications to that FCM token.
+ *
+ * @param senderID The senderID for a particular Firebase project.
+ * @param completion The completion handler to handle the token deletion.
+ */
+- (void)deleteFCMTokenForSenderID:(nonnull NSString *)senderID
+ completion:(nonnull FIRMessagingDeleteFCMTokenCompletion)completion
+ FIR_SWIFT_NAME(deleteFCMToken(forSenderID:completion:));
+
+
+#pragma mark - Connect
+
+/**
+ * Create a FIRMessaging data connection which will be used to send the data notifications
+ * sent by your server. It will also be used to send ACKS and other messages based
+ * on the FIRMessaging ACKS and other messages based on the FIRMessaging protocol.
+ *
+ *
+ * @param handler The handler to be invoked once the connection is established.
+ * If the connection fails we invoke the handler with an
+ * appropriate error code letting you know why it failed. At
+ * the same time, FIRMessaging performs exponential backoff to retry
+ * establishing a connection and invoke the handler when successful.
+ */
+- (void)connectWithCompletion:(nonnull FIRMessagingConnectCompletion)handler
+ FIR_SWIFT_NAME(connect(handler:))
+ __deprecated_msg("Please use the shouldEstablishDirectChannel property instead.");
+
+/**
+ * Disconnect the current FIRMessaging data connection. This stops any attempts to
+ * connect to FIRMessaging. Calling this on an already disconnected client is a no-op.
+ *
+ * Call this before `teardown` when your app is going to the background.
+ * Since the FIRMessaging connection won't be allowed to live when in background it is
+ * prudent to close the connection.
+ */
+- (void)disconnect
+ __deprecated_msg("Please use the shouldEstablishDirectChannel property instead.");
+
+#pragma mark - Topics
+
+/**
+ * Asynchronously subscribes to a topic.
+ *
+ * @param topic The name of the topic, for example, @"sports".
+ */
+- (void)subscribeToTopic:(nonnull NSString *)topic FIR_SWIFT_NAME(subscribe(toTopic:));
+
+/**
+ * Asynchronously unsubscribe from a topic.
+ *
+ * @param topic The name of the topic, for example @"sports".
+ */
+- (void)unsubscribeFromTopic:(nonnull NSString *)topic FIR_SWIFT_NAME(unsubscribe(fromTopic:));
+
+#pragma mark - Upstream
+
+/**
+ * Sends an upstream ("device to cloud") message.
+ *
+ * The message is queued if we don't have an active connection.
+ * You can only use the upstream feature if your FCM implementation
+ * uses the XMPP server protocol.
+ *
+ * @param message Key/Value pairs to be sent. Values must be String, any
+ * other type will be ignored.
+ * @param receiver A string identifying the receiver of the message. For FCM
+ * project IDs the value is `SENDER_ID@gcm.googleapis.com`.
+ * @param messageID The ID of the message. This is generated by the application. It
+ * must be unique for each message generated by this application.
+ * It allows error callbacks and debugging, to uniquely identify
+ * each message.
+ * @param ttl The time to live for the message. In case we aren't able to
+ * send the message before the TTL expires we will send you a
+ * callback. If 0, we'll attempt to send immediately and return
+ * an error if we're not connected. Otherwise, the message will
+ * be queued. As for server-side messages, we don't return an error
+ * if the message has been dropped because of TTL; this can happen
+ * on the server side, and it would require extra communication.
+ */
+- (void)sendMessage:(nonnull NSDictionary *)message
+ to:(nonnull NSString *)receiver
+ withMessageID:(nonnull NSString *)messageID
+ timeToLive:(int64_t)ttl;
+
+#pragma mark - Analytics
+
+/**
+ * Use this to track message delivery and analytics for messages, typically
+ * when you receive a notification in `application:didReceiveRemoteNotification:`.
+ * However, you only need to call this if you set the `FirebaseAppDelegateProxyEnabled`
+ * flag to NO in your Info.plist. If `FirebaseAppDelegateProxyEnabled` is either missing
+ * or set to YES in your Info.plist, the library will call this automatically.
+ *
+ * @param message The downstream message received by the application.
+ *
+ * @return Information about the downstream message.
+ */
+- (nonnull FIRMessagingMessageInfo *)appDidReceiveMessage:(nonnull NSDictionary *)message;
+
+@end
diff --git a/Firebase/Messaging/Public/FirebaseMessaging.h b/Firebase/Messaging/Public/FirebaseMessaging.h
new file mode 100755
index 0000000..ef081c9
--- /dev/null
+++ b/Firebase/Messaging/Public/FirebaseMessaging.h
@@ -0,0 +1,17 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRMessaging.h"
diff --git a/Firebase/Storage/FIRStorage.h b/Firebase/Storage/FIRStorage.h
new file mode 100644
index 0000000..3b37a0e
--- /dev/null
+++ b/Firebase/Storage/FIRStorage.h
@@ -0,0 +1,130 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+#import "FIRStorageConstants.h"
+#import "FIRStorageSwiftNameSupport.h"
+
+@class FIRApp;
+@class FIRStorageReference;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** Project version string for FirebaseStorage. */
+FOUNDATION_EXPORT const unsigned char *const FIRStorageVersionString;
+
+/**
+ * FirebaseStorage is a service that supports uploading and downloading binary objects,
+ * such as images, videos, and other files to Google Cloud Storage.
+ *
+ * If you call [FIRStorage storage], the instance will initialize with the default FIRApp,
+ * [FIRApp defaultApp], and the storage location will come from the provided
+ * GoogleService-Info.plist.
+ *
+ * If you call [FIRStorage storageForApp:] and provide a custom instance of FIRApp,
+ * the storage location will be specified via the FIROptions#storageBucket property.
+ */
+FIR_SWIFT_NAME(Storage)
+@interface FIRStorage : NSObject
+
+/**
+ * Creates an instance of FIRStorage, configured with the default FIRApp.
+ * @return the FIRStorage instance, initialized with the default FIRApp.
+ */
++ (instancetype)storage FIR_SWIFT_NAME(storage());
+
+/**
+ * Creates an instance of FIRStorage, configured with the custom FIRApp @a app.
+ * @param app The custom FIRApp used for initialization.
+ * @return the FIRStorage instance, initialized with the custom FIRApp.
+ */
++ (instancetype)storageForApp:(FIRApp *)app FIR_SWIFT_NAME(storage(app:));
+
+/**
+ * Creates an instance of FIRStorage, configured with a custom storage bucket @a url.
+ * @param url The gs:// url to your Firebase Storage Bucket.
+ * @return the FIRStorage instance, initialized with the custom FIRApp.
+ */
++ (instancetype)storageWithURL:(NSString *)url FIR_SWIFT_NAME(storage(url:));
+
+/**
+ * Creates an instance of FIRStorage, configured with a custom FIRApp @a app and a custom storage
+ * bucket @a url.
+ * @param app The custom FIRApp used for initialization.
+ * @param url The gs:// url to your Firebase Storage Bucket.
+ * @return the FIRStorage instance, initialized with the custom FIRApp.
+ */
++ (instancetype)storageForApp:(FIRApp *)app
+ URL:(NSString *)url FIR_SWIFT_NAME(storage(app:url:));
+
+/**
+ * The Firebase App associated with this Firebase Storage instance.
+ */
+@property(strong, nonatomic, readonly) FIRApp *app;
+
+/**
+ * Maximum time in seconds to retry an upload if a failure occurs.
+ * Defaults to 10 minutes (600 seconds).
+ */
+@property NSTimeInterval maxUploadRetryTime;
+
+/**
+ * Maximum time in seconds to retry a download if a failure occurs.
+ * Defaults to 10 minutes (600 seconds).
+ */
+@property NSTimeInterval maxDownloadRetryTime;
+
+/**
+ * Maximum time in seconds to retry operations other than upload and download if a failure occurs.
+ * Defaults to 2 minutes (120 seconds).
+ */
+@property NSTimeInterval maxOperationRetryTime;
+
+/**
+ * Queue that all developer callbacks are fired on. Defaults to the main queue.
+ */
+@property(strong, nonatomic) dispatch_queue_t callbackQueue;
+
+/**
+ * Creates a FIRStorageReference initialized at the root Firebase Storage location.
+ * @return An instance of FIRStorageReference initialized at the root.
+ */
+- (FIRStorageReference *)reference;
+
+/**
+ * Creates a FIRStorageReference given a gs:// or https:// URL pointing to a Firebase Storage
+ * location. For example, you can pass in an https:// download URL retrieved from
+ * [FIRStorageReference downloadURLWithCompletion] or the gs:// URI from
+ * [FIRStorageReference description].
+ * @param string A gs:// or https:// URL to initialize the reference with.
+ * @return An instance of FIRStorageReference at the given child path.
+ * @throws Throws an exception if passed in URL is not associated with the FIRApp used to initialize
+ * this FIRStorage.
+ */
+- (FIRStorageReference *)referenceForURL:(NSString *)string;
+
+/**
+ * Creates a FIRStorageReference initialized at a child Firebase Storage location.
+ * @param string A relative path from the root to initialize the reference with,
+ * for instance @"path/to/object".
+ * @return An instance of FIRStorageReference at the given child path.
+ */
+- (FIRStorageReference *)referenceWithPath:(NSString *)string;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Storage/FIRStorage.m b/Firebase/Storage/FIRStorage.m
new file mode 100644
index 0000000..dd11391
--- /dev/null
+++ b/Firebase/Storage/FIRStorage.m
@@ -0,0 +1,233 @@
+// Copyright 2017 Google
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "FIRStorage.h"
+
+#import "FIRStorageConstants_Private.h"
+#import "FIRStoragePath.h"
+#import "FIRStorageReference.h"
+#import "FIRStorageReference_Private.h"
+#import "FIRStorageTokenAuthorizer.h"
+#import "FIRStorageUtils.h"
+#import "FIRStorage_Private.h"
+
+#import "FIRApp.h"
+#import "FIROptions.h"
+
+#import <GTMSessionFetcher/GTMSessionFetcher.h>
+#import <GTMSessionFetcher/GTMSessionFetcherLogging.h>
+
+static NSMutableDictionary<
+ NSString * /* app name */,
+ NSMutableDictionary<NSString * /* bucket */, GTMSessionFetcherService *> *> *_fetcherServiceMap;
+static GTMSessionFetcherRetryBlock _retryWhenOffline;
+
+@implementation FIRStorage
+
++ (void)initialize {
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ _retryWhenOffline = ^(BOOL suggestedWillRetry,
+ NSError * GTM_NULLABLE_TYPE error,
+ GTMSessionFetcherRetryResponse response) {
+ bool shouldRetry = suggestedWillRetry;
+ // GTMSessionFetcher does not consider being offline a retryable error, but we do, so we
+ // special-case it here.
+ if (!shouldRetry && error) {
+ shouldRetry = error.code == NSURLErrorNotConnectedToInternet;
+ }
+ response(shouldRetry);
+ };
+ _fetcherServiceMap = [[NSMutableDictionary alloc] init];
+ });
+}
+
++ (GTMSessionFetcherService *)fetcherServiceForApp:(FIRApp *)app bucket:(NSString *)bucket {
+ @synchronized(_fetcherServiceMap) {
+ NSMutableDictionary *bucketMap = _fetcherServiceMap[app.name];
+ if (!bucketMap) {
+ bucketMap = [[NSMutableDictionary alloc] init];
+ _fetcherServiceMap[app.name] = bucketMap;
+ }
+
+ GTMSessionFetcherService *fetcherService = bucketMap[bucket];
+ if (!fetcherService) {
+ fetcherService = [[GTMSessionFetcherService alloc] init];
+ [fetcherService setRetryEnabled:YES];
+ [fetcherService setRetryBlock:_retryWhenOffline];
+ FIRStorageTokenAuthorizer *authorizer =
+ [[FIRStorageTokenAuthorizer alloc] initWithApp:app fetcherService:fetcherService];
+ [fetcherService setAuthorizer:authorizer];
+ bucketMap[bucket] = fetcherService;
+ }
+ return fetcherService;
+ }
+}
+
++ (void)setGTMSessionFetcherLoggingEnabled:(BOOL)isLoggingEnabled {
+ [GTMSessionFetcher setLoggingEnabled:isLoggingEnabled];
+}
+
++ (instancetype)storage {
+ return [self storageForApp:[FIRApp defaultApp]];
+}
+
++ (instancetype)storageForApp:(FIRApp *)app {
+ NSString* url;
+
+ if (app.options.storageBucket) {
+ url = [app.options.storageBucket isEqualToString:@""] ? @""
+ : [@"gs://" stringByAppendingString:app.options.storageBucket];
+ }
+
+ return [self storageForApp:app URL:url];
+}
+
++ (instancetype)storageWithURL:(NSString *)url {
+ return [self storageForApp:[FIRApp defaultApp] URL:url];
+}
+
++ (instancetype)storageForApp:(FIRApp *)app URL:(NSString *)url {
+ if (!url) {
+ NSString *const kAppNotConfiguredMessage =
+ @"No default Storage bucket found. Did you configure Firebase Storage properly?";
+ [NSException raise:NSInvalidArgumentException format:kAppNotConfiguredMessage];
+ }
+
+ NSString *bucket;
+ if ([url isEqualToString:@""]) {
+ bucket = @"";
+ } else {
+ FIRStoragePath *path;
+
+ @try {
+ path = [FIRStoragePath pathFromGSURI:url];
+ } @catch (NSException *e) {
+ [NSException raise:NSInternalInconsistencyException
+ format:@"URI must be in the form of gs://<bucket>/"];
+ }
+
+ if (path.object != nil && ![path.object isEqualToString:@""]) {
+ [NSException raise:NSInternalInconsistencyException
+ format:@"Storage bucket cannot be initialized with a path"];
+ }
+
+ bucket = path.bucket;
+ }
+
+ return [[self alloc] initWithApp:app bucket:bucket];
+}
+
+- (instancetype)initWithApp:(FIRApp *)app bucket:(NSString *)bucket {
+ self = [super init];
+ if (self) {
+ _app = app;
+ _storageBucket = bucket;
+ _fetcherServiceForApp = [FIRStorage fetcherServiceForApp:_app bucket:bucket];
+ _maxDownloadRetryTime = 600.0;
+ _maxOperationRetryTime = 120.0;
+ _maxUploadRetryTime = 600.0;
+ }
+ return self;
+}
+
+#pragma mark - NSObject overrides
+
+- (instancetype)copyWithZone:(NSZone *)zone {
+ FIRStorage *storage = [[[self class] allocWithZone:zone] initWithApp:_app bucket:_storageBucket];
+ storage.callbackQueue = _callbackQueue;
+ return storage;
+}
+
+// Two FIRStorage objects are equal if they use the same app
+- (BOOL)isEqual:(id)object {
+ if (self == object) {
+ return YES;
+ }
+
+ if (![object isKindOfClass:[FIRStorage class]]) {
+ return NO;
+ }
+
+ BOOL isEqualObject = [self isEqualToFIRStorage:(FIRStorage *)object];
+ return isEqualObject;
+}
+
+- (BOOL)isEqualToFIRStorage:(FIRStorage *)storage {
+ BOOL isEqual = [_app isEqual:storage->_app];
+ return isEqual;
+}
+
+- (NSUInteger)hash {
+ NSUInteger hash = [_app hash] ^ [_callbackQueue hash];
+ return hash;
+}
+
+- (NSString *)description {
+ return [NSString stringWithFormat:@"%@ %p: %@", [self class], self, _app];
+}
+
+#pragma mark - Public methods
+
+- (FIRStorageReference *)reference {
+ FIRStoragePath *path = [[FIRStoragePath alloc] initWithBucket:_storageBucket object:nil];
+ return [[FIRStorageReference alloc] initWithStorage:self path:path];
+}
+
+- (FIRStorageReference *)referenceForURL:(NSString *)string {
+ FIRStoragePath *path = [FIRStoragePath pathFromString:string];
+
+ // If no default bucket exists (empty string), accept anything.
+ if ([_storageBucket isEqual:@""]) {
+ FIRStorageReference *reference = [[FIRStorageReference alloc] initWithStorage:self path:path];
+ return reference;
+ }
+
+ // If there exists a default bucket, throw if provided a different bucket.
+ if (![path.bucket isEqual:_storageBucket]) {
+ NSString *const kInvalidBucketFormat =
+ @"Provided bucket: %@ does not match the Storage bucket of the current instance: %@";
+ [NSException raise:NSInvalidArgumentException
+ format:kInvalidBucketFormat, path.bucket, _storageBucket];
+ }
+
+ FIRStorageReference *reference = [[FIRStorageReference alloc] initWithStorage:self path:path];
+ return reference;
+}
+
+- (FIRStorageReference *)referenceWithPath:(NSString *)string {
+ FIRStorageReference *reference = [[self reference] child:string];
+ return reference;
+}
+
+- (void)setCallbackQueue:(dispatch_queue_t)callbackQueue {
+ _fetcherServiceForApp.callbackQueue = callbackQueue;
+}
+
+#pragma mark - Background tasks
+
++ (void)enableBackgroundTasks:(BOOL)isEnabled {
+ [NSException raise:NSGenericException format:@"enableBackgroundTasks not implemented"];
+}
+
+- (NSArray<FIRStorageUploadTask *> *)uploadTasks {
+ [NSException raise:NSGenericException format:@"getUploadTasks not implemented"];
+ return nil;
+}
+
+- (NSArray<FIRStorageDownloadTask *> *)downloadTasks {
+ [NSException raise:NSGenericException format:@"getDownloadTasks not implemented"];
+ return nil;
+}
+@end
diff --git a/Firebase/Storage/FIRStorageConstants.h b/Firebase/Storage/FIRStorageConstants.h
new file mode 100644
index 0000000..cf6c3b8
--- /dev/null
+++ b/Firebase/Storage/FIRStorageConstants.h
@@ -0,0 +1,173 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+#import "FIRStorageSwiftNameSupport.h"
+
+@class FIRStorageDownloadTask;
+@class FIRStorageMetadata;
+@class FIRStorageTaskSnapshot;
+@class FIRStorageUploadTask;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * NSString typedef representing a task listener handle.
+ */
+typedef NSString *FIRStorageHandle FIR_SWIFT_NAME(StorageHandle);
+
+/**
+ * Block typedef typically used when downloading data.
+ * @param data The data returned by the download, or nil if no data available or download failed.
+ * @param error The error describing failure, if one occurred.
+ */
+typedef void (^FIRStorageVoidDataError)(NSData *_Nullable data, NSError *_Nullable error)
+ FIR_SWIFT_NAME(StorageVoidDataError);
+
+/**
+ * Block typedef typically used when performing "binary" async operations such as delete,
+ * where the operation either succeeds without an error or fails with an error.
+ * @param error The error describing failure, if one occurred.
+ */
+typedef void (^FIRStorageVoidError)(NSError *_Nullable error) FIR_SWIFT_NAME(StorageVoidError);
+
+/**
+ * Block typedef typically used when retrieving metadata.
+ * @param metadata The metadata returned by the operation, if metadata exists.
+ */
+typedef void (^FIRStorageVoidMetadata)(FIRStorageMetadata *_Nullable metadata)
+ FIR_SWIFT_NAME(StorageVoidMetadata);
+
+/**
+ * Block typedef typically used when retrieving metadata with the possibility of an error.
+ * @param metadata The metadata returned by the operation, if metadata exists.
+ * @param error The error describing failure, if one occurred.
+ */
+typedef void (^FIRStorageVoidMetadataError)(FIRStorageMetadata *_Nullable metadata,
+ NSError *_Nullable error)
+ FIR_SWIFT_NAME(StorageVoidMetadataError);
+
+/**
+ * Block typedef typically used to asynchronously return a storage task snapshot.
+ * @param snapshot The returned task snapshot.
+ */
+typedef void (^FIRStorageVoidSnapshot)(FIRStorageTaskSnapshot *snapshot)
+ FIR_SWIFT_NAME(StorageVoidSnapshot);
+
+/**
+ * Block typedef typically used when retrieving a download URL.
+ * @param URL The download URL associated with the operation.
+ * @param error The error describing failure, if one occurred.
+ */
+typedef void (^FIRStorageVoidURLError)(NSURL *_Nullable URL, NSError *_Nullable error)
+ FIR_SWIFT_NAME(StorageVoidURLError);
+
+/**
+ * Enum representing the upload and download task status.
+ */
+typedef NS_ENUM(NSInteger, FIRStorageTaskStatus) {
+ /**
+ * Unknown task status.
+ */
+ FIRStorageTaskStatusUnknown,
+
+ /**
+ * Task is being resumed.
+ */
+ FIRStorageTaskStatusResume,
+
+ /**
+ * Task reported a progress event.
+ */
+ FIRStorageTaskStatusProgress,
+
+ /**
+ * Task is paused.
+ */
+ FIRStorageTaskStatusPause,
+
+ /**
+ * Task has completed successfully.
+ */
+ FIRStorageTaskStatusSuccess,
+
+ /**
+ * Task has failed and is unrecoverable.
+ */
+ FIRStorageTaskStatusFailure
+} FIR_SWIFT_NAME(StorageTaskStatus);
+
+/**
+ * Firebase Storage error domain.
+ */
+FOUNDATION_EXPORT NSString *const FIRStorageErrorDomain FIR_SWIFT_NAME(StorageErrorDomain);
+
+/**
+ * Enum representing the errors raised by Firebase Storage.
+ */
+typedef NS_ENUM(NSInteger, FIRStorageErrorCode) {
+ /** An unknown error occurred. */
+ FIRStorageErrorCodeUnknown = -13000,
+
+ /** No object exists at the desired reference. */
+ FIRStorageErrorCodeObjectNotFound = -13010,
+
+ /** No bucket is configured for Firebase Storage. */
+ FIRStorageErrorCodeBucketNotFound = -13011,
+
+ /** No project is configured for Firebase Storage. */
+ FIRStorageErrorCodeProjectNotFound = -13012,
+
+ /**
+ * Quota on your Firebase Storage bucket has been exceeded.
+ * If you're on the free tier, upgrade to a paid plan.
+ * If you're on a paid plan, reach out to Firebase support.
+ */
+ FIRStorageErrorCodeQuotaExceeded = -13013,
+
+ /** User is unauthenticated. Authenticate and try again. */
+ FIRStorageErrorCodeUnauthenticated = -13020,
+
+ /**
+ * User is not authorized to perform the desired action.
+ * Check your rules to ensure they are correct.
+ */
+ FIRStorageErrorCodeUnauthorized = -13021,
+
+ /**
+ * The maximum time limit on an operation (upload, download, delete, etc.) has been exceeded.
+ * Try uploading again.
+ */
+ FIRStorageErrorCodeRetryLimitExceeded = -13030,
+
+ /**
+ * File on the client does not match the checksum of the file received by the server.
+ * Try uploading again.
+ */
+ FIRStorageErrorCodeNonMatchingChecksum = -13031,
+
+ /**
+ * Size of the downloaded file exceeds the amount of memory allocated for the download.
+ * Increase memory cap and try downloading again.
+ */
+ FIRStorageErrorCodeDownloadSizeExceeded = -13032,
+
+ /** User cancelled the operation. */
+ FIRStorageErrorCodeCancelled = -13040
+} FIR_SWIFT_NAME(StorageErrorCode);
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Storage/FIRStorageConstants.m b/Firebase/Storage/FIRStorageConstants.m
new file mode 100644
index 0000000..aa3da1b
--- /dev/null
+++ b/Firebase/Storage/FIRStorageConstants.m
@@ -0,0 +1,83 @@
+// Copyright 2017 Google
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "FIRStorageConstants.h"
+
+#import "FIRStorageConstants_Private.h"
+
+NSString *const kGCSScheme = @"https";
+NSString *const kGCSHost = @"www.googleapis.com";
+NSString *const kGCSUploadPath = @"upload";
+NSString *const kGCSStorageVersionPath = @"storage/v1";
+NSString *const kGCSBucketPathFormat = @"b/%@";
+NSString *const kGCSObjectPathFormat = @"o/%@";
+
+NSString *const kFIRStorageScheme = @"https";
+NSString *const kFIRStorageHost = @"firebasestorage.googleapis.com";
+NSString *const kFIRStorageVersionPath = @"v0";
+NSString *const kFIRStorageBucketPathFormat = @"b/%@";
+NSString *const kFIRStorageObjectPathFormat = @"o/%@";
+NSString *const kFIRStorageFullPathFormat = @"/v0/b/%@/o/%@";
+
+NSString *const kFIRStorageAuthTokenFormat = @"Firebase %@";
+NSString *const kFIRStorageDefaultBucketFormat = @"gs://%@";
+
+NSString *const kFIRStorageResponseErrorDomain = @"ResponseErrorDomain";
+NSString *const kFIRStorageResponseErrorCode = @"ResponseErrorCode";
+NSString *const kFIRStorageResponseBody = @"ResponseBody";
+
+NSString *const FIRStorageErrorDomain = @"FIRStorageErrorDomain";
+
+NSString *const kFIRStorageInvalidDataFormat = @"Invalid data returned from the server: %@";
+NSString *const kFIRStorageInvalidObserverStatus = @"Invalid observer status requested, use one "
+ @"of: FIRStorageTaskStatusPause, Resume, Progress, " @"Complete, or Failure";
+
+/**
+ * String constants mapping GCS Object#resource mappings to metadata fields.
+ */
+NSString *const kFIRStorageMetadataBucket = @"bucket";
+NSString *const kFIRStorageMetadataCacheControl = @"cacheControl";
+NSString *const kFIRStorageMetadataContentDisposition = @"contentDisposition";
+NSString *const kFIRStorageMetadataContentEncoding = @"contentEncoding";
+NSString *const kFIRStorageMetadataContentLanguage = @"contentLanguage";
+NSString *const kFIRStorageMetadataContentType = @"contentType";
+NSString *const kFIRStorageMetadataCustomMetadata = @"metadata";
+NSString *const kFIRStorageMetadataSize = @"size";
+NSString *const kFIRStorageMetadataDownloadURLs = @"downloadURLs";
+NSString *const kFIRStorageMetadataGeneration = @"generation";
+NSString *const kFIRStorageMetadataMetageneration = @"metageneration";
+NSString *const kFIRStorageMetadataTimeCreated = @"timeCreated";
+NSString *const kFIRStorageMetadataUpdated = @"updated";
+NSString *const kFIRStorageMetadataName = @"name";
+NSString *const kFIRStorageMetadataDownloadTokens = @"downloadTokens";
+
+// TODO: add notification support
+NSString *const kFIRStorageTaskStatusResumeNotification =
+ @"kFIRStorageTaskStatusResumeNotification";
+NSString *const kFIRStorageTaskStatusPauseNotification = @"kFIRStorageTaskStatusResumeNotification";
+NSString *const kFIRStorageTaskStatusProgressNotification =
+ @"kFIRStorageTaskStatusResumeNotification";
+NSString *const kFIRStorageTaskStatusCompleteNotification =
+ @"kFIRStorageTaskStatusResumeNotification";
+NSString *const kFIRStorageTaskStatusFailureNotification =
+ @"kFIRStorageTaskStatusResumeNotification";
+
+NSString *const kFIRStorageBundleIdentifier = @"com.google.firebase.storage";
+
+// The STR and STR_EXPAND macro allow a numeric version passed to he compiler driver
+// with a -D to be treated as a string instead of an invalid floating point value.
+#define STR(x) STR_EXPAND(x)
+#define STR_EXPAND(x) #x
+const unsigned char *const FIRStorageVersionString =
+ (const unsigned char *const) STR(FIRStorage_VERSION);
diff --git a/Firebase/Storage/FIRStorageDeleteTask.m b/Firebase/Storage/FIRStorageDeleteTask.m
new file mode 100644
index 0000000..4f3f1cc
--- /dev/null
+++ b/Firebase/Storage/FIRStorageDeleteTask.m
@@ -0,0 +1,54 @@
+// Copyright 2017 Google
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "FIRStorageDeleteTask.h"
+
+#import "FIRStorageTask_Private.h"
+
+@implementation FIRStorageDeleteTask {
+ @private
+ FIRStorageVoidError _completion;
+}
+
+- (instancetype)initWithReference:(FIRStorageReference *)reference
+ fetcherService:(GTMSessionFetcherService *)service
+ completion:(FIRStorageVoidError)completion {
+ self = [super initWithReference:reference fetcherService:service];
+ if (self) {
+ _completion = [completion copy];
+ }
+ return self;
+}
+
+- (void)enqueue {
+ NSMutableURLRequest *request = [self.baseRequest mutableCopy];
+ request.HTTPMethod = @"DELETE";
+ request.timeoutInterval = self.reference.storage.maxOperationRetryTime;
+
+ FIRStorageVoidError callback = _completion;
+ _completion = nil;
+
+ GTMSessionFetcher *fetcher = [self.fetcherService fetcherWithRequest:request];
+ fetcher.comment = @"DeleteTask";
+ [fetcher beginFetchWithCompletionHandler:^(NSData *_Nullable data, NSError *_Nullable error) {
+ if (!self.error) {
+ self.error = [FIRStorageErrors errorWithServerError:error reference:self.reference];
+ }
+ if (callback) {
+ callback(self.error);
+ }
+ }];
+}
+
+@end
diff --git a/Firebase/Storage/FIRStorageDownloadTask.h b/Firebase/Storage/FIRStorageDownloadTask.h
new file mode 100644
index 0000000..252b910
--- /dev/null
+++ b/Firebase/Storage/FIRStorageDownloadTask.h
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+#import "FIRStorageObservableTask.h"
+#import "FIRStorageSwiftNameSupport.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * FIRStorageDownloadTask implements resumable downloads from an object in Firebase Storage.
+ * Downloads can be returned on completion with a completion handler, and can be monitored
+ * by attaching observers, or controlled by calling FIRStorageTask#pause, FIRStorageTask#resume,
+ * or FIRStorageTask#cancel.
+ * Downloads can currently be returned as NSData in memory, or as an NSURL to a file on disk.
+ * Downloads are performed on a background queue, and callbacks are raised on the developer
+ * specified callbackQueue in FIRStorage, or the main queue if left unspecified.
+ * Currently all uploads must be initiated and managed on the main queue.
+ */
+FIR_SWIFT_NAME(StorageDownloadTask)
+@interface FIRStorageDownloadTask : FIRStorageObservableTask<FIRStorageTaskManagement>
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Storage/FIRStorageDownloadTask.m b/Firebase/Storage/FIRStorageDownloadTask.m
new file mode 100644
index 0000000..0d71e52
--- /dev/null
+++ b/Firebase/Storage/FIRStorageDownloadTask.m
@@ -0,0 +1,162 @@
+// Copyright 2017 Google
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "FIRStorageDownloadTask.h"
+
+#import "FIRStorageConstants_Private.h"
+#import "FIRStorageDownloadTask_Private.h"
+#import "FIRStorageObservableTask_Private.h"
+#import "FIRStorageTask_Private.h"
+
+@implementation FIRStorageDownloadTask
+
+@synthesize progress = _progress;
+@synthesize fetcher = _fetcher;
+
+- (instancetype)initWithReference:(FIRStorageReference *)reference
+ fetcherService:(GTMSessionFetcherService *)service
+ file:(nullable NSURL *)fileURL {
+ self = [super initWithReference:reference fetcherService:service];
+ if (self) {
+ _fileURL = [fileURL copy];
+ _progress = [NSProgress progressWithTotalUnitCount:0];
+ }
+ return self;
+}
+
+- (void)enqueue {
+ [self enqueueWithData:nil];
+}
+
+- (void)enqueueWithData:(nullable NSData *)resumeData {
+ NSAssert([NSThread isMainThread], @"Download attempting to execute on non main queue! Please "
+ @"only execute this method on the main queue.");
+ self.state = FIRStorageTaskStateQueueing;
+ NSMutableURLRequest *request = [self.baseRequest mutableCopy];
+ request.HTTPMethod = @"GET";
+ request.timeoutInterval = self.reference.storage.maxDownloadRetryTime;
+ NSURLComponents *components =
+ [NSURLComponents componentsWithURL:request.URL resolvingAgainstBaseURL:NO];
+ [components setQuery:@"alt=media"];
+ request.URL = components.URL;
+
+ GTMSessionFetcher *fetcher;
+ if (resumeData) {
+ fetcher = [GTMSessionFetcher fetcherWithDownloadResumeData:resumeData];
+ fetcher.comment = @"Resuming DownloadTask";
+ } else {
+ fetcher = [self.fetcherService fetcherWithRequest:request];
+ fetcher.comment = @"Starting DownloadTask";
+ }
+
+ [fetcher setResumeDataBlock:^(NSData *data) {
+ if (data) {
+ _downloadData = data;
+ }
+ }];
+
+ fetcher.maxRetryInterval = self.reference.storage.maxDownloadRetryTime;
+
+ if (_fileURL) {
+ // Handle file downloads
+ [fetcher setDestinationFileURL:_fileURL];
+ [fetcher setDownloadProgressBlock:^(int64_t bytesWritten, int64_t totalBytesWritten,
+ int64_t totalBytesExpectedToWrite) {
+ self.state = FIRStorageTaskStateProgress;
+ self.progress.completedUnitCount = totalBytesWritten;
+ self.progress.totalUnitCount = totalBytesExpectedToWrite;
+ FIRStorageTaskSnapshot *snapshot = self.snapshot;
+ [self fireHandlersForStatus:FIRStorageTaskStatusProgress snapshot:snapshot];
+ self.state = FIRStorageTaskStateRunning;
+ }];
+ } else {
+ // Handle data downloads
+ [fetcher setReceivedProgressBlock:^(int64_t bytesWritten, int64_t totalBytesWritten) {
+ self.state = FIRStorageTaskStateProgress;
+ self.progress.completedUnitCount = totalBytesWritten;
+ int64_t totalLength = [[self.fetcher response] expectedContentLength];
+ self.progress.totalUnitCount = totalLength;
+ FIRStorageTaskSnapshot *snapshot = self.snapshot;
+ [self fireHandlersForStatus:FIRStorageTaskStatusProgress snapshot:snapshot];
+ self.state = FIRStorageTaskStateRunning;
+ }];
+ }
+
+ _fetcher = fetcher;
+
+ self.state = FIRStorageTaskStateRunning;
+ [self.fetcher beginFetchWithCompletionHandler:^(NSData *data, NSError *error) {
+ // Fire last progress updates
+ [self fireHandlersForStatus:FIRStorageTaskStatusProgress snapshot:self.snapshot];
+
+ // Handle potential issues with download
+ if (error) {
+ self.state = FIRStorageTaskStateFailed;
+ self.error = [FIRStorageErrors errorWithServerError:error reference:self.reference];
+ [self fireHandlersForStatus:FIRStorageTaskStatusFailure snapshot:self.snapshot];
+ [self removeAllObservers];
+ return;
+ }
+
+ // Download completed successfully, fire completion callbacks
+ self.state = FIRStorageTaskStateSuccess;
+
+ if (data) {
+ _downloadData = data;
+ }
+
+ [self fireHandlersForStatus:FIRStorageTaskStatusSuccess snapshot:self.snapshot];
+ [self removeAllObservers];
+ }];
+}
+
+#pragma mark - Download Management
+
+- (void)cancel {
+ NSError *error = [FIRStorageErrors errorWithCode:FIRStorageErrorCodeCancelled];
+ [self cancelWithError:error];
+}
+
+- (void)cancelWithError:(NSError *)error {
+ NSAssert([NSThread isMainThread], @"Cancel attempting to execute on non main queue! Please only "
+ @"execute this method on the main queue.");
+ self.state = FIRStorageTaskStateCancelled;
+ [self.fetcher stopFetching];
+ self.error = error;
+ [self fireHandlersForStatus:FIRStorageTaskStatusFailure snapshot:self.snapshot];
+}
+
+- (void)pause {
+ NSAssert([NSThread isMainThread], @"Pause attempting to execute on non main queue! Please only "
+ @"execute this method on the main queue.");
+ self.state = FIRStorageTaskStatePausing;
+ [self.fetcher stopFetching];
+ // Give the resume callback a chance to run (if scheduled)
+ [self.fetcher waitForCompletionWithTimeout:0.001];
+ self.state = FIRStorageTaskStatePaused;
+ FIRStorageTaskSnapshot *snapshot = self.snapshot;
+ [self fireHandlersForStatus:FIRStorageTaskStatusPause snapshot:snapshot];
+}
+
+- (void)resume {
+ NSAssert([NSThread isMainThread], @"Resume attempting to execute on non main queue! Please only "
+ @"execute this method on the main queue.");
+ self.state = FIRStorageTaskStateResuming;
+ FIRStorageTaskSnapshot *snapshot = self.snapshot;
+ [self fireHandlersForStatus:FIRStorageTaskStatusResume snapshot:snapshot];
+ self.state = FIRStorageTaskStateRunning;
+ [self enqueueWithData:_downloadData];
+}
+
+@end
diff --git a/Firebase/Storage/FIRStorageErrors.m b/Firebase/Storage/FIRStorageErrors.m
new file mode 100644
index 0000000..49a5ffa
--- /dev/null
+++ b/Firebase/Storage/FIRStorageErrors.m
@@ -0,0 +1,172 @@
+// Copyright 2017 Google
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "FIRStorageErrors.h"
+
+#import "FIRStorageConstants_Private.h"
+#import "FIRStorageReference.h"
+#import "FIRStorageReference_Private.h"
+
+@implementation FIRStorageErrors
+
++ (NSError *)errorWithCode:(FIRStorageErrorCode)code {
+ return [FIRStorageErrors errorWithCode:code infoDictionary:nil];
+}
+
++ (NSError *)errorWithCode:(FIRStorageErrorCode)code
+ infoDictionary:(nullable NSDictionary *)dictionary {
+ NSMutableDictionary *errorDictionary;
+ if (dictionary) {
+ errorDictionary = [dictionary mutableCopy];
+ } else {
+ errorDictionary = [[NSMutableDictionary alloc] init];
+ }
+
+ NSString *errorMessage;
+ switch (code) {
+ case FIRStorageErrorCodeObjectNotFound:
+ errorMessage =
+ [NSString stringWithFormat:@"Object %@ does not exist.", errorDictionary[@"object"]];
+ break;
+
+ case FIRStorageErrorCodeBucketNotFound:
+ errorMessage =
+ [NSString stringWithFormat:@"Bucket %@ does not exist.", errorDictionary[@"bucket"]];
+ break;
+
+ case FIRStorageErrorCodeProjectNotFound:
+ errorMessage =
+ [NSString stringWithFormat:@"Project %@ does not exist.", errorDictionary[@"project"]];
+ break;
+
+ case FIRStorageErrorCodeQuotaExceeded: {
+ NSString *const kQuotaExceededFormat =
+ @"Quota for bucket %@ exceeded, please view quota on firebase.google.com.";
+ errorMessage = [NSString stringWithFormat:kQuotaExceededFormat, errorDictionary[@"bucket"]];
+ break;
+ }
+
+ case FIRStorageErrorCodeDownloadSizeExceeded: {
+ int64_t total = [errorDictionary[@"totalSize"] longLongValue];
+ int64_t size = [errorDictionary[@"maxAllowedSize"] longLongValue];
+ NSString *totalString = total ? @(total).stringValue : @"unknown";
+ NSString *sizeString = total ? @(size).stringValue : @"unknown";
+ NSString *const kSizeExceededErrorFormat =
+ @"Attempeted to download object with size of %@ bytes, "
+ @"which exceeds the maximum size of %@ bytes. "
+ @"Consider raising the maximum download size, or using "
+ @"[FIRStorageReference writeToFile:]";
+ errorMessage = [NSString stringWithFormat:kSizeExceededErrorFormat, totalString, sizeString];
+ break;
+ }
+
+ case FIRStorageErrorCodeUnauthenticated:
+ errorMessage = @"User is not authenticated, please authenticate using Firebase "
+ @"Authentication and try again.";
+ break;
+
+ case FIRStorageErrorCodeUnauthorized: {
+ NSString *bucket = errorDictionary[@"bucket"];
+ NSString *object = errorDictionary[@"object"];
+ NSString *const kUnauthorizedFormat = @"User does not have permission to access gs://%@/%@.";
+ errorMessage = [NSString stringWithFormat:kUnauthorizedFormat, bucket, object];
+ break;
+ }
+
+ case FIRStorageErrorCodeRetryLimitExceeded:
+ errorMessage = @"Max retry time for operation exceeded, please try again.";
+ break;
+
+ case FIRStorageErrorCodeNonMatchingChecksum: {
+ // TODO: replace with actual checksum strings when we choose to implement.
+ NSString *const kChecksumFailedErrorFormat =
+ @"Uploaded/downloaded object %@ has checksum: %@ "
+ @"which does not match server checksum: %@. Please retry the upload/download.";
+ errorMessage = [NSString stringWithFormat:kChecksumFailedErrorFormat, @"object",
+ @"client checksum", @"server checksum"];
+ break;
+ }
+
+ case FIRStorageErrorCodeCancelled:
+ errorMessage = @"User cancelled the upload/download.";
+ break;
+
+ case FIRStorageErrorCodeUnknown:
+ /* Fall through to default case for unknown errors */
+
+ default:
+ errorMessage = @"An unknown error occurred, please check the server response.";
+ break;
+ }
+
+ errorDictionary[NSLocalizedDescriptionKey] = errorMessage;
+
+ NSError *err = [NSError errorWithDomain:FIRStorageErrorDomain code:code userInfo:errorDictionary];
+ return err;
+}
+
++ (nullable NSError *)errorWithServerError:(nullable NSError *)error
+ reference:(nullable FIRStorageReference *)reference {
+ if (error == nil) {
+ return nil;
+ }
+
+ FIRStorageErrorCode errorCode;
+ switch (error.code) {
+ case 400:
+ errorCode = FIRStorageErrorCodeUnknown;
+ break;
+
+ case 401:
+ errorCode = FIRStorageErrorCodeUnauthenticated;
+ break;
+
+ case 402:
+ errorCode = FIRStorageErrorCodeQuotaExceeded;
+ break;
+
+ case 403:
+ errorCode = FIRStorageErrorCodeUnauthorized;
+ break;
+
+ case 404:
+ errorCode = FIRStorageErrorCodeObjectNotFound;
+ break;
+
+ default:
+ errorCode = FIRStorageErrorCodeUnknown;
+ break;
+ }
+
+ NSMutableDictionary *errorDictionary =
+ [[[NSDictionary alloc] initWithDictionary:error.userInfo] mutableCopy];
+ errorDictionary[kFIRStorageResponseErrorDomain] = error.domain;
+ errorDictionary[kFIRStorageResponseErrorCode] = @(error.code);
+
+ // Turn raw response into a string
+ NSData *responseData = errorDictionary[@"data"];
+ if (responseData) {
+ NSString *errorString =
+ [[NSString alloc] initWithData:responseData encoding:NSUTF8StringEncoding];
+ errorDictionary[kFIRStorageResponseBody] = errorString ?: @"No Response from Server.";
+ }
+
+ errorDictionary[@"bucket"] = reference.path.bucket;
+ errorDictionary[@"object"] = reference.path.object;
+
+ NSError *clientError = [FIRStorageErrors errorWithCode:errorCode infoDictionary:errorDictionary];
+ return clientError;
+}
+
+@end
diff --git a/Firebase/Storage/FIRStorageGetMetadataTask.m b/Firebase/Storage/FIRStorageGetMetadataTask.m
new file mode 100644
index 0000000..d0e8981
--- /dev/null
+++ b/Firebase/Storage/FIRStorageGetMetadataTask.m
@@ -0,0 +1,84 @@
+// Copyright 2017 Google
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "FIRStorageGetMetadataTask.h"
+
+#import "FIRStorageConstants.h"
+#import "FIRStorageMetadata_Private.h"
+#import "FIRStorageTask_Private.h"
+#import "FIRStorageUtils.h"
+
+#import "FirebaseStorage.h"
+
+@implementation FIRStorageGetMetadataTask {
+ @private
+ FIRStorageVoidMetadataError _completion;
+}
+
+- (instancetype)initWithReference:(FIRStorageReference *)reference
+ fetcherService:(GTMSessionFetcherService *)service
+ completion:(FIRStorageVoidMetadataError)completion {
+ self = [super initWithReference:reference fetcherService:service];
+ if (self) {
+ _completion = [completion copy];
+ }
+ return self;
+}
+
+- (void)enqueue {
+ NSMutableURLRequest *request = [self.baseRequest mutableCopy];
+ request.HTTPMethod = @"GET";
+ request.timeoutInterval = self.reference.storage.maxDownloadRetryTime;
+
+ FIRStorageVoidMetadataError callback = _completion;
+ _completion = nil;
+
+ GTMSessionFetcher *fetcher = [self.fetcherService fetcherWithRequest:request];
+ fetcher.comment = @"GetMetadataTask";
+ [fetcher beginFetchWithCompletionHandler:^(NSData *data, NSError *error) {
+ if (error) {
+ if (!self.error) {
+ self.error = [FIRStorageErrors errorWithServerError:error reference:self.reference];
+ }
+ if (callback) {
+ callback(nil, self.error);
+ }
+ return;
+ }
+
+ NSDictionary *responseDictionary = [NSDictionary frs_dictionaryFromJSONData:data];
+ if (responseDictionary != nil) {
+ FIRStorageMetadata *metadata =
+ [[FIRStorageMetadata alloc] initWithDictionary:responseDictionary];
+ [metadata setType:FIRStorageMetadataTypeFile];
+ if (callback) {
+ callback(metadata, nil);
+ }
+ } else {
+ NSString *returnedData = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
+ NSString *invalidDataString =
+ [NSString stringWithFormat:kFIRStorageInvalidDataFormat, returnedData];
+ NSDictionary *dict;
+ if (invalidDataString.length > 0) {
+ dict = @{NSLocalizedFailureReasonErrorKey : invalidDataString};
+ }
+ self.error = [FIRStorageErrors errorWithCode:FIRStorageErrorCodeUnknown infoDictionary:dict];
+ if (callback) {
+ callback(nil, self.error);
+ }
+ }
+ }];
+}
+
+@end
diff --git a/Firebase/Storage/FIRStorageMetadata.h b/Firebase/Storage/FIRStorageMetadata.h
new file mode 100644
index 0000000..8d844f7
--- /dev/null
+++ b/Firebase/Storage/FIRStorageMetadata.h
@@ -0,0 +1,149 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+#import "FIRStorageSwiftNameSupport.h"
+
+@class FIRStorageReference;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * Class which represents the metadata on an object in Firebase Storage. This metadata is
+ * returned on successful operations, and can be used to retrieve download URLs, content types,
+ * and a FIRStorage reference to the object in question. Full documentation can be found at the GCS
+ * Objects#resource docs.
+ * @see https://cloud.google.com/storage/docs/json_api/v1/objects#resource
+ */
+FIR_SWIFT_NAME(StorageMetadata)
+@interface FIRStorageMetadata : NSObject<NSCopying>
+
+/**
+ * The name of the bucket containing this object.
+ */
+@property(copy, nonatomic, readonly) NSString *bucket;
+
+/**
+ * Cache-Control directive for the object data.
+ */
+@property(copy, nonatomic, nullable) NSString *cacheControl;
+
+/**
+ * Content-Disposition of the object data.
+ */
+@property(copy, nonatomic, nullable) NSString *contentDisposition;
+
+/**
+ * Content-Encoding of the object data.
+ */
+@property(copy, nonatomic, nullable) NSString *contentEncoding;
+
+/**
+ * Content-Language of the object data.
+ */
+@property(copy, nonatomic, nullable) NSString *contentLanguage;
+
+/**
+ * Content-Type of the object data.
+ */
+@property(copy, nonatomic, nullable) NSString *contentType;
+
+/**
+ * The content generation of this object. Used for object versioning.
+ */
+@property(readonly) int64_t generation;
+
+/**
+ * User-provided metadata, in key/value pairs.
+ */
+@property(copy, nonatomic, nullable) NSDictionary<NSString *, NSString *> *customMetadata;
+
+/**
+ * The version of the metadata for this object at this generation. Used
+ * for preconditions and for detecting changes in metadata. A metageneration number is only
+ * meaningful in the context of a particular generation of a particular object.
+ */
+@property(readonly) int64_t metageneration;
+
+/**
+ * The name of this object, in gs://bucket/path/to/object.txt, this is object.txt.
+ */
+@property(copy, nonatomic, readonly, nullable) NSString *name;
+
+/**
+ * The full path of this object, in gs://bucket/path/to/object.txt, this is path/to/object.txt.
+ */
+@property(copy, nonatomic, readonly, nullable) NSString *path;
+
+/**
+ * Content-Length of the data in bytes.
+ */
+@property(readonly) int64_t size;
+
+/**
+ * The creation time of the object in RFC 3339 format.
+ */
+@property(copy, nonatomic, readonly, nullable) NSDate *timeCreated;
+
+/**
+ * The modification time of the object metadata in RFC 3339 format.
+ */
+@property(copy, nonatomic, readonly, nullable) NSDate *updated;
+
+/**
+ * A reference to the object in Firebase Storage.
+ */
+@property(strong, nonatomic, readonly, nullable) FIRStorageReference *storageReference;
+
+/**
+ * An array containing all download URLs available for the object.
+ */
+@property(strong, nonatomic, readonly, nullable) NSArray<NSURL *> *downloadURLs;
+
+/**
+ * Creates an instanece of FIRStorageMetadata from the contents of a dictionary.
+ * @return An instance of FIRStorageMetadata that represents the contents of a dictionary.
+ */
+- (nullable instancetype)initWithDictionary:(NSDictionary <NSString *, id>*)dictionary
+ NS_DESIGNATED_INITIALIZER;
+
+/**
+ * Creates an NSDictionary from the contents of the metadata.
+ * @return An NSDictionary that represents the contents of the metadata.
+ */
+- (NSDictionary <NSString *, id>*)dictionaryRepresentation;
+
+/**
+ * Determines if the current metadata represents a "file".
+ */
+@property(readonly, getter=isFile) BOOL file;
+
+/**
+ * Determines if the current metadata represents a "folder".
+ */
+@property(readonly, getter=isFolder) BOOL folder;
+
+/**
+ * Retrieves a download URL for the given object, or nil if none exist.
+ * Note that if there are many valid download tokens, this will always return the first
+ * valid token created.
+ */
+- (nullable NSURL *)downloadURL;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Storage/FIRStorageMetadata.m b/Firebase/Storage/FIRStorageMetadata.m
new file mode 100644
index 0000000..6c85bbf
--- /dev/null
+++ b/Firebase/Storage/FIRStorageMetadata.m
@@ -0,0 +1,227 @@
+// Copyright 2017 Google
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "FIRStorageMetadata.h"
+
+#import "FIRStorageConstants.h"
+#import "FIRStorageConstants_Private.h"
+#import "FIRStorageMetadata_Private.h"
+#import "FIRStorageUtils.h"
+
+// TODO: consider rewriting this using GTLR (GTLRStorageObjects.h)
+@implementation FIRStorageMetadata
+
+#pragma mark - Initializers
+
+- (instancetype)init {
+ return [self initWithDictionary:[NSDictionary dictionary]];
+}
+
+- (instancetype)initWithDictionary:(NSDictionary *)dictionary {
+ self = [super init];
+ if (self) {
+ _bucket = dictionary[kFIRStorageMetadataBucket];
+ _cacheControl = dictionary[kFIRStorageMetadataCacheControl];
+ _contentDisposition = dictionary[kFIRStorageMetadataContentDisposition];
+ _contentEncoding = dictionary[kFIRStorageMetadataContentEncoding];
+ _contentLanguage = dictionary[kFIRStorageMetadataContentLanguage];
+ _contentType = dictionary[kFIRStorageMetadataContentType];
+ _customMetadata = dictionary[kFIRStorageMetadataCustomMetadata];
+ _size = [dictionary[kFIRStorageMetadataSize] longLongValue];
+ _downloadURLs = dictionary[kFIRStorageMetadataDownloadURLs];
+ _generation = [dictionary[kFIRStorageMetadataGeneration] longLongValue];
+ _metageneration = [dictionary[kFIRStorageMetadataMetageneration] longLongValue];
+ _timeCreated = [self dateFromRFC3339String:dictionary[kFIRStorageMetadataTimeCreated]];
+ _updated = [self dateFromRFC3339String:dictionary[kFIRStorageMetadataUpdated]];
+ // GCS "name" is our path, our "name" is just the last path component of the path
+ _path = dictionary[kFIRStorageMetadataName];
+ _name = [_path lastPathComponent];
+ NSString *downloadTokens = dictionary[kFIRStorageMetadataDownloadTokens];
+ if (downloadTokens) {
+ NSArray<NSString *> *downloadStringArray = [downloadTokens componentsSeparatedByString:@","];
+ NSMutableArray<NSURL *> *downloadURLArray =
+ [[NSMutableArray alloc] initWithCapacity:[downloadStringArray count]];
+ [downloadStringArray enumerateObjectsUsingBlock:^(NSString *_Nonnull token, NSUInteger idx,
+ BOOL *_Nonnull stop) {
+ NSURLComponents *components = [[NSURLComponents alloc] init];
+ components.scheme = kFIRStorageScheme;
+ components.host = kFIRStorageHost;
+ NSString *path = [FIRStorageUtils GCSEscapedString:_path];
+ NSString *fullPath = [NSString stringWithFormat:kFIRStorageFullPathFormat, _bucket, path];
+ components.percentEncodedPath = fullPath;
+ components.query = [NSString stringWithFormat:@"alt=media&token=%@", token];
+
+ [downloadURLArray insertObject:[components URL] atIndex:idx];
+ }];
+ _downloadURLs = downloadURLArray;
+ }
+ }
+ return self;
+}
+
+#pragma mark - NSObject overrides
+
+- (instancetype)copyWithZone:(NSZone *)zone {
+ return [[[self class] allocWithZone:zone] initWithDictionary:[self dictionaryRepresentation]];
+}
+
+- (BOOL)isEqual:(id)object {
+ if (self == object) {
+ return YES;
+ }
+
+ if (![object isKindOfClass:[FIRStorageMetadata class]]) {
+ return NO;
+ }
+
+ BOOL isEqualObject = [self isEqualToFIRStorageMetadata:(FIRStorageMetadata *)object];
+ return isEqualObject;
+}
+
+- (BOOL)isEqualToFIRStorageMetadata:(FIRStorageMetadata *)metadata {
+ return [[self dictionaryRepresentation] isEqualToDictionary:[metadata dictionaryRepresentation]];
+}
+
+- (NSUInteger)hash {
+ NSUInteger hash = [[self dictionaryRepresentation] hash];
+ return hash;
+}
+
+- (NSString *)description {
+ NSDictionary *metadataDictionary = [self dictionaryRepresentation];
+ return [NSString stringWithFormat:@"%@ %p: %@", [self class], self, metadataDictionary];
+}
+
+#pragma mark - Public methods
+
+- (NSDictionary *)dictionaryRepresentation {
+ NSMutableDictionary *metadataDictionary = [[NSMutableDictionary alloc] initWithCapacity:13];
+
+ if (_bucket) {
+ metadataDictionary[kFIRStorageMetadataBucket] = _bucket;
+ }
+
+ if (_cacheControl) {
+ metadataDictionary[kFIRStorageMetadataCacheControl] = _cacheControl;
+ }
+
+ if (_contentDisposition) {
+ metadataDictionary[kFIRStorageMetadataContentDisposition] = _contentDisposition;
+ }
+
+ if (_contentEncoding) {
+ metadataDictionary[kFIRStorageMetadataContentEncoding] = _contentEncoding;
+ }
+
+ if (_contentLanguage) {
+ metadataDictionary[kFIRStorageMetadataContentLanguage] = _contentLanguage;
+ }
+
+ if (_contentType) {
+ metadataDictionary[kFIRStorageMetadataContentType] = _contentType;
+ }
+
+ if (_customMetadata) {
+ metadataDictionary[kFIRStorageMetadataCustomMetadata] = _customMetadata;
+ }
+
+ if (_downloadURLs) {
+ NSMutableArray *downloadTokens = [[NSMutableArray alloc] init];
+ [_downloadURLs
+ enumerateObjectsUsingBlock:^(NSURL *_Nonnull URL, NSUInteger idx, BOOL *_Nonnull stop) {
+ NSArray *queryItems = [URL.query componentsSeparatedByString:@"&"];
+ [queryItems enumerateObjectsUsingBlock:^(NSString *queryString, NSUInteger idx,
+ BOOL *_Nonnull stop) {
+ NSString *key;
+ NSString *value;
+ NSScanner *scanner = [NSScanner scannerWithString:queryString];
+ [scanner scanUpToString:@"=" intoString:&key];
+ [scanner scanString:@"=" intoString:NULL];
+ [scanner scanUpToString:@"\n" intoString:&value];
+ if ([key isEqual:@"token"]) {
+ [downloadTokens addObject:value];
+ *stop = YES;
+ }
+ }];
+ }];
+ NSString *downloadTokenString = [downloadTokens componentsJoinedByString:@","];
+ metadataDictionary[kFIRStorageMetadataDownloadTokens] = downloadTokenString;
+ }
+
+ if (_generation) {
+ NSString *generationString = [NSString stringWithFormat:@"%lld", _generation];
+ metadataDictionary[kFIRStorageMetadataGeneration] = generationString;
+ }
+
+ if (_metageneration) {
+ NSString *metagenerationString = [NSString stringWithFormat:@"%lld", _metageneration];
+ metadataDictionary[kFIRStorageMetadataMetageneration] = metagenerationString;
+ }
+
+ if (_timeCreated) {
+ metadataDictionary[kFIRStorageMetadataTimeCreated] = [self RFC3339StringFromDate:_timeCreated];
+ }
+
+ if (_updated) {
+ metadataDictionary[kFIRStorageMetadataUpdated] = [self RFC3339StringFromDate:_updated];
+ }
+
+ if (_path) {
+ metadataDictionary[kFIRStorageMetadataName] = _path;
+ }
+
+ return [metadataDictionary copy];
+}
+
+- (BOOL)isFile {
+ return _type == FIRStorageMetadataTypeFile;
+}
+
+- (BOOL)isFolder {
+ return _type == FIRStorageMetadataTypeFolder;
+}
+
+- (nullable NSURL *)downloadURL {
+ return [_downloadURLs firstObject];
+}
+
+#pragma mark - RFC 3339 conversions
+
+static NSDateFormatter *sRFC3339DateFormatter;
+
+static void setupDateFormatterOnce(void) {
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ sRFC3339DateFormatter = [[NSDateFormatter alloc] init];
+ NSLocale *enUSPOSIXLocale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"];
+
+ [sRFC3339DateFormatter setLocale:enUSPOSIXLocale];
+ [sRFC3339DateFormatter setDateFormat:@"yyyy'-'MM'-'dd'T'HH':'mm':'ss.SSSZZZZZ"];
+ [sRFC3339DateFormatter setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0]];
+ });
+}
+
+- (nullable NSDate *)dateFromRFC3339String:(NSString *)dateString {
+ setupDateFormatterOnce();
+ NSDate *rfc3339Date = [sRFC3339DateFormatter dateFromString:dateString];
+ return rfc3339Date;
+}
+
+- (nullable NSString *)RFC3339StringFromDate:(NSDate *)date {
+ setupDateFormatterOnce();
+ NSString *rfc3339String = [sRFC3339DateFormatter stringFromDate:date];
+ return rfc3339String;
+}
+
+@end
diff --git a/Firebase/Storage/FIRStorageObservableTask.h b/Firebase/Storage/FIRStorageObservableTask.h
new file mode 100644
index 0000000..502aba5
--- /dev/null
+++ b/Firebase/Storage/FIRStorageObservableTask.h
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRStorageSwiftNameSupport.h"
+#import "FIRStorageTask.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@class FIRStorageReference;
+@class FIRStorageTaskSnapshot;
+
+/**
+ * Extends FIRStorageTask to provide observable semantics such as adding and removing observers.
+ * Observers produce a FIRStorageHandle, which is used to keep track of and remove specific
+ * observers at a later date.
+ * This class is currently not thread safe and can only be called on the main thread.
+ */
+FIR_SWIFT_NAME(StorageObservableTask)
+@interface FIRStorageObservableTask : FIRStorageTask
+
+/**
+ * Observes changes in the upload status: Resume, Pause, Progress, Success, and Failure.
+ * @param status The FIRStorageTaskStatus change to observe.
+ * @param handler A callback that fires every time the status event occurs,
+ * returns a FIRStorageTaskSnapshot containing the state of the task.
+ * @return A task handle that can be used to remove the observer at a later date.
+ */
+- (FIRStorageHandle)observeStatus:(FIRStorageTaskStatus)status
+ handler:(void (^)(FIRStorageTaskSnapshot *snapshot))handler;
+
+/**
+ * Removes the single observer with the provided handle.
+ * @param handle The handle of the task to remove.
+ */
+- (void)removeObserverWithHandle:(FIRStorageHandle)handle;
+
+/**
+ * Removes all observers for a single status.
+ * @param status A FIRStorageTaskStatus to remove listeners for.
+ */
+- (void)removeAllObserversForStatus:(FIRStorageTaskStatus)status;
+
+/**
+ * Removes all observers.
+ */
+- (void)removeAllObservers;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Storage/FIRStorageObservableTask.m b/Firebase/Storage/FIRStorageObservableTask.m
new file mode 100644
index 0000000..bac5924
--- /dev/null
+++ b/Firebase/Storage/FIRStorageObservableTask.m
@@ -0,0 +1,216 @@
+// Copyright 2017 Google
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "FIRStorageObservableTask.h"
+#import "FIRStorageObservableTask_Private.h"
+#import "FIRStorageTask_Private.h"
+
+@implementation FIRStorageObservableTask {
+ @private
+ // Handlers for pause, resume, progress, success, and failure callbacks
+ NSMutableDictionary<NSString *, FIRStorageVoidSnapshot> *_resumeHandlers;
+ NSMutableDictionary<NSString *, FIRStorageVoidSnapshot> *_pauseHandlers;
+ NSMutableDictionary<NSString *, FIRStorageVoidSnapshot> *_progressHandlers;
+ NSMutableDictionary<NSString *, FIRStorageVoidSnapshot> *_successHandlers;
+ NSMutableDictionary<NSString *, FIRStorageVoidSnapshot> *_failureHandlers;
+ // Reverse map of fetcher handles to status types
+ NSMutableDictionary<NSString *, NSNumber *> *_handleToStatusMap;
+}
+
+@synthesize state = _state;
+
+- (instancetype)initWithReference:(FIRStorageReference *)reference
+ fetcherService:(GTMSessionFetcherService *)service {
+ self = [super initWithReference:reference fetcherService:service];
+ if (self) {
+ _pauseHandlers = [[NSMutableDictionary alloc] init];
+ _resumeHandlers = [[NSMutableDictionary alloc] init];
+ _progressHandlers = [[NSMutableDictionary alloc] init];
+ _successHandlers = [[NSMutableDictionary alloc] init];
+ _failureHandlers = [[NSMutableDictionary alloc] init];
+ _handleToStatusMap = [[NSMutableDictionary alloc] init];
+ }
+ return self;
+}
+
+#pragma mark - Observers
+
+- (FIRStorageHandle)observeStatus:(FIRStorageTaskStatus)status
+ handler:(FIRStorageVoidSnapshot)handler {
+ FIRStorageVoidSnapshot callback = handler;
+ handler = nil;
+
+ // Note: self.snapshot is synchronized
+ FIRStorageTaskSnapshot *snapshot = self.snapshot;
+ // TODO: use an increasing counter instead of a random UUID
+ NSString *UUIDString = [[NSUUID UUID] UUIDString];
+ switch (status) {
+ case FIRStorageTaskStatusPause:
+ @synchronized(self) {
+ [_pauseHandlers setValue:callback forKey:UUIDString];
+ } // @synchronized(self)
+ if (_state == FIRStorageTaskStatePausing || _state == FIRStorageTaskStatePaused) {
+ [self fireHandlers:_pauseHandlers snapshot:snapshot];
+ }
+ break;
+
+ case FIRStorageTaskStatusResume:
+ @synchronized(self) {
+ [_resumeHandlers setValue:callback forKey:UUIDString];
+ } // @synchronized(self)
+ if (_state == FIRStorageTaskStateResuming || _state == FIRStorageTaskStateRunning) {
+ [self fireHandlers:_resumeHandlers snapshot:snapshot];
+ }
+ break;
+
+ case FIRStorageTaskStatusProgress:
+ @synchronized(self) {
+ [_progressHandlers setValue:callback forKey:UUIDString];
+ } // @synchronized(self)
+ if (_state == FIRStorageTaskStateRunning || _state == FIRStorageTaskStateProgress) {
+ [self fireHandlers:_progressHandlers snapshot:snapshot];
+ }
+ break;
+
+ case FIRStorageTaskStatusSuccess:
+ @synchronized(self) {
+ [_successHandlers setValue:callback forKey:UUIDString];
+ } // @synchronized(self)
+ if (_state == FIRStorageTaskStateSuccess) {
+ [self fireHandlers:_successHandlers snapshot:snapshot];
+ }
+ break;
+
+ case FIRStorageTaskStatusFailure:
+ @synchronized(self) {
+ [_failureHandlers setValue:callback forKey:UUIDString];
+ } // @synchronized(self)
+ if (_state == FIRStorageTaskStateFailing || _state == FIRStorageTaskStateFailed) {
+ [self fireHandlers:_failureHandlers snapshot:snapshot];
+ }
+ break;
+
+ case FIRStorageTaskStatusUnknown:
+ // Fall through to exception case if an unknown status is passed
+
+ default:
+ [NSException raise:NSInternalInconsistencyException
+ format:kFIRStorageInvalidObserverStatus, nil];
+ break;
+ }
+
+ @synchronized(self) {
+ _handleToStatusMap[UUIDString] = @(status);
+ } // @synchronized(self)
+
+ return UUIDString;
+}
+
+- (void)removeObserverWithHandle:(FIRStorageHandle)handle {
+ FIRStorageTaskStatus status = [_handleToStatusMap[handle] intValue];
+ NSMutableDictionary<NSString *, FIRStorageVoidSnapshot> *observerDictionary =
+ [self handlerDictionaryForStatus:status];
+
+ @synchronized(self) {
+ [observerDictionary removeObjectForKey:handle];
+ [_handleToStatusMap removeObjectForKey:handle];
+ } // @synchronized(self)
+}
+
+- (void)removeAllObserversForStatus:(FIRStorageTaskStatus)status {
+ NSMutableDictionary<NSString *, FIRStorageVoidSnapshot> *observerDictionary =
+ [self handlerDictionaryForStatus:status];
+ [self removeHandlersFromStatusMapForDictionary:observerDictionary];
+
+ @synchronized(self) {
+ [observerDictionary removeAllObjects];
+ } // @synchronized(self)
+}
+
+- (void)removeAllObservers {
+ @synchronized(self) {
+ [_pauseHandlers removeAllObjects];
+ [_resumeHandlers removeAllObjects];
+ [_progressHandlers removeAllObjects];
+ [_successHandlers removeAllObjects];
+ [_failureHandlers removeAllObjects];
+ [_handleToStatusMap removeAllObjects];
+ } // @synchronized(self)
+}
+
+- (NSMutableDictionary<NSString *, FIRStorageVoidSnapshot> *)handlerDictionaryForStatus:
+ (FIRStorageTaskStatus)status {
+ switch (status) {
+ case FIRStorageTaskStatusPause:
+ return _pauseHandlers;
+
+ case FIRStorageTaskStatusResume:
+ return _resumeHandlers;
+
+ case FIRStorageTaskStatusProgress:
+ return _progressHandlers;
+
+ case FIRStorageTaskStatusSuccess:
+ return _successHandlers;
+
+ case FIRStorageTaskStatusFailure:
+ return _failureHandlers;
+
+ case FIRStorageTaskStatusUnknown:
+ return [NSMutableDictionary dictionary];
+
+ default:
+ [NSException raise:NSInternalInconsistencyException
+ format:kFIRStorageInvalidObserverStatus, nil];
+ return nil;
+ }
+}
+
+- (void)removeHandlersFromStatusMapForDictionary:
+ (NSMutableDictionary<NSString *, FIRStorageVoidSnapshot> *)dict {
+ @synchronized(self) {
+ [_handleToStatusMap removeObjectsForKeys:dict.allKeys];
+ } // @synchronized(self)
+}
+
+- (void)fireHandlersForStatus:(FIRStorageTaskStatus)status
+ snapshot:(FIRStorageTaskSnapshot *)snapshot {
+ NSMutableDictionary<NSString *, FIRStorageVoidSnapshot> *observerDictionary =
+ [self handlerDictionaryForStatus:status];
+ [self fireHandlers:observerDictionary snapshot:snapshot];
+}
+
+- (void)fireHandlers:(NSMutableDictionary<NSString *, FIRStorageVoidSnapshot> *)handlers
+ snapshot:(FIRStorageTaskSnapshot *)snapshot {
+ dispatch_queue_t callbackQueue = self.fetcherService.callbackQueue;
+ if (!callbackQueue) {
+ callbackQueue = dispatch_get_main_queue();
+ }
+
+ // TODO: iterate over this list in a consistent order
+ NSMutableDictionary<NSString *, FIRStorageVoidSnapshot> *handlersCopy;
+ @synchronized(self) {
+ handlersCopy = [handlers copy];
+ } // @synchronized(self)
+ [handlersCopy enumerateKeysAndObjectsUsingBlock:^(NSString *_Nonnull key,
+ FIRStorageVoidSnapshot _Nonnull handler,
+ BOOL *_Nonnull stop) {
+
+ dispatch_async(callbackQueue, ^{
+ handler(snapshot);
+ });
+ }];
+}
+
+@end
diff --git a/Firebase/Storage/FIRStoragePath.m b/Firebase/Storage/FIRStoragePath.m
new file mode 100644
index 0000000..7188ab6
--- /dev/null
+++ b/Firebase/Storage/FIRStoragePath.m
@@ -0,0 +1,199 @@
+// Copyright 2017 Google
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "FIRStoragePath.h"
+
+#import "FIRStorageConstants_Private.h"
+
+@implementation FIRStoragePath
+
+#pragma mark - Class methods
+
++ (nullable FIRStoragePath *)pathFromString:(NSString *)string {
+ if ([string hasPrefix:@"gs://"]) {
+ // "gs://bucket/path/to/object"
+ return [FIRStoragePath pathFromGSURI:string];
+ } else if ([string hasPrefix:@"http://"] || [string hasPrefix:@"https://"]) {
+ // "http[s]://firebasestorage.googleapis.com/bucket/path/to/object?signed_url_params"
+ return [FIRStoragePath pathFromHTTPURL:string];
+ } else {
+ // Invalid scheme, raise an exception!
+ [NSException raise:NSInternalInconsistencyException
+ format:@"URL scheme must be one of gs://, http://, or https:// "];
+ return nil;
+ }
+}
+
++ (nullable FIRStoragePath *)pathFromGSURI:(NSString *)aURIString {
+ NSString *bucketName;
+ NSString *objectName;
+ NSScanner *scanner = [NSScanner scannerWithString:aURIString];
+ BOOL isGSURI = [scanner scanString:@"gs://" intoString:NULL];
+ BOOL hasBucket = [scanner scanUpToString:@"/" intoString:&bucketName];
+ [scanner scanString:@"/" intoString:NULL];
+ [scanner scanUpToString:@"\n" intoString:&objectName];
+
+ if (!isGSURI || !hasBucket) {
+ [NSException raise:NSInternalInconsistencyException
+ format:@"URI must be in the form of gs://<bucket>/<path/to/object>"];
+ return nil;
+ }
+
+ return [[self alloc] initWithBucket:bucketName object:objectName];
+}
+
++ (nullable FIRStoragePath *)pathFromHTTPURL:(NSString *)aURLString {
+ NSString *bucketName;
+ NSString *objectName;
+ NSURL *httpsURL = [NSURL URLWithString:aURLString];
+ NSArray *pathComponents = httpsURL.pathComponents; // [/, v0, b, <bucket>, o, <objects/...>]
+
+ if ([httpsURL.host isEqual:kFIRStorageHost]) {
+ // Have a bucket name
+ if ([pathComponents count] > 3) {
+ bucketName = pathComponents[3];
+ }
+
+ // Have an object name
+ if ([pathComponents count] > 5) {
+ NSRange objectRange = NSMakeRange(5, [pathComponents count] - 5);
+ objectName = [[pathComponents subarrayWithRange:objectRange] componentsJoinedByString:@"/"];
+ }
+ }
+
+ if (bucketName.length == 0) {
+ [NSException raise:NSInternalInconsistencyException
+ format:@"URL must be in the form of "
+ @"http[s]://firebasestorage.googleapis.com/v0/b/<bucket>/o/<path/to/"
+ @"object>[?token=signed_url_params]"];
+ return nil;
+ }
+
+ if (objectName.length == 0) {
+ objectName = nil;
+ }
+
+ return [[self alloc] initWithBucket:bucketName object:objectName];
+}
+
+#pragma mark - Initializers
+
+- (instancetype)initWithBucket:(NSString *)bucket object:(nullable NSString *)object {
+ self = [super init];
+ if (self) {
+ _bucket = [bucket copy];
+ _object = [self standardizedPathForString:[object copy]];
+ }
+ return self;
+}
+
+#pragma mark - NSObject overrides
+
+- (instancetype)copyWithZone:(NSZone *)zone {
+ return [[[self class] allocWithZone:zone] initWithBucket:_bucket object:_object];
+}
+
+- (BOOL)isEqual:(id)object {
+ if (self == object) {
+ return YES;
+ }
+
+ if (![object isKindOfClass:[FIRStoragePath class]]) {
+ return NO;
+ }
+
+ BOOL isObjectEqual = [self isEqualToFIRStoragePath:(FIRStoragePath *)object];
+ return isObjectEqual;
+}
+
+- (BOOL)isEqualToFIRStoragePath:(FIRStoragePath *)path {
+ BOOL isBucketEqual = _bucket == nil && path->_bucket == nil;
+ BOOL isObjectEqual = _object == nil && path->_object == nil;
+
+ if (_bucket && path->_bucket) {
+ isBucketEqual = [_bucket isEqual:path->_bucket];
+ }
+
+ if (_object && path.object) {
+ isObjectEqual = [_object isEqual:path->_object];
+ }
+
+ BOOL isEqual = isBucketEqual && isObjectEqual;
+ return isEqual;
+}
+
+- (NSUInteger)hash {
+ // "...because in those days, you could XOR anything with anything and get something useful..."
+ // https://www.usenix.org/system/files/1309_14-17_mickens.pdf
+ NSUInteger hash = [_bucket hash] ^ [_object hash];
+ return hash;
+}
+
+- (NSString *)description {
+ return [NSString stringWithFormat:@"%@ %p: %@", [self class], self, [self stringValue]];
+}
+
+- (NSString *)stringValue {
+ return [NSString stringWithFormat:@"gs://%@/%@", _bucket, _object ?: @""];
+}
+
+#pragma mark - Public methods
+
+- (FIRStoragePath *)child:(NSString *)path {
+ if (path.length == 0) {
+ return [self copy]; // Return a copy of the same path, nothing happened
+ }
+
+ NSString *childObject;
+ if (_object == nil) {
+ childObject = path;
+ } else {
+ childObject = [_object stringByAppendingPathComponent:path];
+ }
+
+ FIRStoragePath *childPath = [[FIRStoragePath alloc] initWithBucket:_bucket object:childObject];
+ return childPath;
+}
+
+- (nullable FIRStoragePath *)parent {
+ if (_object.length == 0) {
+ return nil;
+ }
+
+ NSString *parentObject = [_object stringByDeletingLastPathComponent];
+ FIRStoragePath *parentPath = [[FIRStoragePath alloc] initWithBucket:_bucket object:parentObject];
+ return parentPath;
+}
+
+- (FIRStoragePath *)root {
+ FIRStoragePath *rootPath = [[FIRStoragePath alloc] initWithBucket:_bucket object:nil];
+ return rootPath;
+}
+
+#pragma mark - Private methods
+
+// Removes leading and trailing slashes, and compresses multiple slashes
+// to create a canonical representation.
+// Example: /foo//bar///baz//// -> foo/bar/baz
+- (NSString *)standardizedPathForString:(NSString *)string {
+ NSMutableArray *components = [[string componentsSeparatedByString:@"/"] mutableCopy];
+ NSIndexSet *removedPaths =
+ [components indexesOfObjectsPassingTest:^BOOL(NSString *string, NSUInteger idx, BOOL *stop) {
+ return (string.length == 0);
+ }];
+ [components removeObjectsAtIndexes:removedPaths];
+ return [components componentsJoinedByString:@"/"];
+}
+
+@end
diff --git a/Firebase/Storage/FIRStorageReference.h b/Firebase/Storage/FIRStorageReference.h
new file mode 100644
index 0000000..0b85267
--- /dev/null
+++ b/Firebase/Storage/FIRStorageReference.h
@@ -0,0 +1,244 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+#import "FIRStorage.h"
+#import "FIRStorageConstants.h"
+#import "FIRStorageDownloadTask.h"
+#import "FIRStorageMetadata.h"
+#import "FIRStorageSwiftNameSupport.h"
+#import "FIRStorageTask.h"
+#import "FIRStorageUploadTask.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * FIRStorageReference represents a reference to a Google Cloud Storage object. Developers can
+ * upload and download objects, as well as get/set object metadata, and delete an object at the
+ * path.
+ * @see https://cloud.google.com/storage/
+ */
+FIR_SWIFT_NAME(StorageReference)
+@interface FIRStorageReference : NSObject
+
+/**
+ * The FIRStorage service object which created this reference.
+ */
+@property(nonatomic, readonly) FIRStorage *storage;
+
+/**
+ * The name of the Google Cloud Storage bucket associated with this reference,
+ * in gs://bucket/path/to/object.txt, the bucket would be: 'bucket'
+ */
+@property(nonatomic, readonly) NSString *bucket;
+
+/**
+ * The full path to this object, not including the Google Cloud Storage bucket.
+ * In gs://bucket/path/to/object.txt, the full path would be: 'path/to/object.txt'
+ */
+@property(nonatomic, readonly) NSString *fullPath;
+
+/**
+ * The short name of the object associated with this reference,
+ * in gs://bucket/path/to/object.txt, the name of the object would be: 'object.txt'
+ */
+@property(nonatomic, readonly) NSString *name;
+
+#pragma mark - Path Operations
+
+/**
+ * Creates a new FIRStorageReference pointing to the root object.
+ * @return A new FIRStorageReference pointing to the root object.
+ */
+- (FIRStorageReference *)root;
+
+/**
+ * Creates a new FIRStorageReference pointing to the parent of the current reference
+ * or nil if this instance references the root location.
+ * For example:
+ * path = foo/bar/baz parent = foo/bar
+ * path = foo parent = (root)
+ * path = (root) parent = nil
+ * @return A new FIRStorageReference pointing to the parent of the current reference.
+ */
+- (nullable FIRStorageReference *)parent;
+
+/**
+ * Creates a new FIRStorageReference pointing to a child object of the current reference.
+ * path = foo child = bar newPath = foo/bar
+ * path = foo/bar child = baz newPath = foo/bar/baz
+ * All leading and trailing slashes will be removed, and consecutive slashes will be
+ * compressed to single slashes. For example:
+ * child = /foo/bar newPath = foo/bar
+ * child = foo/bar/ newPath = foo/bar
+ * child = foo///bar newPath = foo/bar
+ * @param path Path to append to the current path.
+ * @return A new FIRStorageReference pointing to a child location of the current reference.
+ */
+- (FIRStorageReference *)child:(NSString *)path;
+
+#pragma mark - Uploads
+
+/**
+ * Asynchronously uploads data to the currently specified FIRStorageReference,
+ * without additional metadata.
+ * This is not recommended for large files, and one should instead upload a file from disk.
+ * @param uploadData The NSData to upload.
+ * @return An instance of FIRStorageUploadTask, which can be used to monitor or manage the upload.
+ */
+- (FIRStorageUploadTask *)putData:(NSData *)uploadData FIR_SWIFT_NAME(putData(_:));
+
+/**
+ * Asynchronously uploads data to the currently specified FIRStorageReference.
+ * This is not recommended for large files, and one should instead upload a file from disk.
+ * @param uploadData The NSData to upload.
+ * @param metadata FIRStorageMetadata containing additional information (MIME type, etc.)
+ * about the object being uploaded.
+ * @return An instance of FIRStorageUploadTask, which can be used to monitor or manage the upload.
+ */
+- (FIRStorageUploadTask *)putData:(NSData *)uploadData
+ metadata:(nullable FIRStorageMetadata *)metadata
+ FIR_SWIFT_NAME(putData(_:metadata:));
+
+/**
+ * Asynchronously uploads data to the currently specified FIRStorageReference.
+ * This is not recommended for large files, and one should instead upload a file from disk.
+ * @param uploadData The NSData to upload.
+ * @param metadata FIRStorageMetadata containing additional information (MIME type, etc.)
+ * about the object being uploaded.
+ * @param completion A completion block that either returns the object metadata on success,
+ * or an error on failure.
+ * @return An instance of FIRStorageUploadTask, which can be used to monitor or manage the upload.
+ */
+- (FIRStorageUploadTask *)putData:(NSData *)uploadData
+ metadata:(nullable FIRStorageMetadata *)metadata
+ completion:(nullable void (^)(FIRStorageMetadata *_Nullable metadata,
+ NSError *_Nullable error))completion
+ FIR_SWIFT_NAME(putData(_:metadata:completion:));
+
+/**
+ * Asynchronously uploads a file to the currently specified FIRStorageReference,
+ * without additional metadata.
+ * @param fileURL A URL representing the system file path of the object to be uploaded.
+ * @return An instance of FIRStorageUploadTask, which can be used to monitor or manage the upload.
+ */
+- (FIRStorageUploadTask *)putFile:(NSURL *)fileURL FIR_SWIFT_NAME(putFile(from:));
+
+/**
+ * Asynchronously uploads a file to the currently specified FIRStorageReference.
+ * @param fileURL A URL representing the system file path of the object to be uploaded.
+ * @param metadata FIRStorageMetadata containing additional information (MIME type, etc.)
+ * about the object being uploaded.
+ * @return An instance of FIRStorageUploadTask, which can be used to monitor or manage the upload.
+ */
+- (FIRStorageUploadTask *)putFile:(NSURL *)fileURL metadata:(nullable FIRStorageMetadata *)metadata
+ FIR_SWIFT_NAME(putFile(from:metadata:));
+
+/**
+ * Asynchronously uploads a file to the currently specified FIRStorageReference.
+ * @param fileURL A URL representing the system file path of the object to be uploaded.
+ * @param metadata FIRStorageMetadata containing additional information (MIME type, etc.)
+ * about the object being uploaded.
+ * @param completion A completion block that either returns the object metadata on success,
+ * or an error on failure.
+ * @return An instance of FIRStorageUploadTask, which can be used to monitor or manage the upload.
+ */
+- (FIRStorageUploadTask *)putFile:(NSURL *)fileURL
+ metadata:(nullable FIRStorageMetadata *)metadata
+ completion:(nullable void (^)(FIRStorageMetadata *_Nullable metadata,
+ NSError *_Nullable error))completion
+ FIR_SWIFT_NAME(putFile(from:metadata:completion:));
+
+#pragma mark - Downloads
+
+/**
+ * Asynchronously downloads the object at the FIRStorageReference to an NSData object in memory.
+ * An NSData of the provided max size will be allocated, so ensure that the device has enough free
+ * memory to complete the download. For downloading large files, writeToFile may be a better option.
+ * @param size The maximum size in bytes to download. If the download exceeds this size
+ * the task will be cancelled and an error will be returned.
+ * @param completion A completion block that either returns the object data on success,
+ * or an error on failure.
+ * @return An FIRStorageDownloadTask that can be used to monitor or manage the download.
+ */
+- (FIRStorageDownloadTask *)dataWithMaxSize:(int64_t)size
+ completion:(void (^)(NSData *_Nullable data,
+ NSError *_Nullable error))completion
+ FIR_SWIFT_NAME(getData(maxSize:completion:));
+
+/**
+ * Asynchronously retrieves a long lived download URL with a revokable token.
+ * This can be used to share the file with others, but can be revoked by a developer
+ * in the Firebase Console if desired.
+ * @param completion A completion block that either returns the URL on success,
+ * or an error on failure.
+ */
+- (void)downloadURLWithCompletion:(void (^)(NSURL *_Nullable URL,
+ NSError *_Nullable error))completion;
+
+/**
+ * Asynchronously downloads the object at the current path to a specified system filepath.
+ * @param fileURL A file system URL representing the path the object should be downloaded to.
+ * @return An FIRStorageDownloadTask that can be used to monitor or manage the download.
+ */
+- (FIRStorageDownloadTask *)writeToFile:(NSURL *)fileURL;
+
+/**
+ * Asynchronously downloads the object at the current path to a specified system filepath.
+ * @param fileURL A file system URL representing the path the object should be downloaded to.
+ * @param completion A completion block that fires when the file download completes.
+ * Returns an NSURL pointing to the file path of the downloaded file on success,
+ * or an error on failure.
+ * @return An FIRStorageDownloadTask that can be used to monitor or manage the download.
+ */
+- (FIRStorageDownloadTask *)writeToFile:(NSURL *)fileURL
+ completion:(nullable void (^)(NSURL *_Nullable URL,
+ NSError *_Nullable error))completion;
+
+#pragma mark - Metadata Operations
+
+/**
+ * Retrieves metadata associated with an object at the current path.
+ * @param completion A completion block which returns the object metadata on success,
+ * or an error on failure.
+ */
+- (void)metadataWithCompletion:(void (^)(FIRStorageMetadata *_Nullable metadata,
+ NSError *_Nullable error))completion
+ FIR_SWIFT_NAME(getMetadata(completion:));
+
+/**
+ * Updates the metadata associated with an object at the current path.
+ * @param metadata An FIRStorageMetadata object with the metadata to update.
+ * @param completion A completion block which returns the FIRStorageMetadata on success,
+ * or an error on failure.
+ */
+- (void)updateMetadata:(FIRStorageMetadata *)metadata
+ completion:(nullable void (^)(FIRStorageMetadata *_Nullable metadata,
+ NSError *_Nullable error))completion
+ FIR_SWIFT_NAME(updateMetadata(_:completion:));
+
+#pragma mark - Delete
+
+/**
+ * Deletes the object at the current path.
+ * @param completion A completion block which returns nil on success, or an error on failure.
+ */
+- (void)deleteWithCompletion:(nullable void (^)(NSError *_Nullable error))completion;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Storage/FIRStorageReference.m b/Firebase/Storage/FIRStorageReference.m
new file mode 100644
index 0000000..6e8105c
--- /dev/null
+++ b/Firebase/Storage/FIRStorageReference.m
@@ -0,0 +1,364 @@
+// Copyright 2017 Google
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "FIRStorageReference.h"
+
+#import "FIRStorageConstants_Private.h"
+#import "FIRStorageDeleteTask.h"
+#import "FIRStorageDownloadTask_Private.h"
+#import "FIRStorageGetMetadataTask.h"
+#import "FIRStorageMetadata_Private.h"
+#import "FIRStorageReference_Private.h"
+#import "FIRStorageTaskSnapshot.h"
+#import "FIRStorageTaskSnapshot_Private.h"
+#import "FIRStorageTask_Private.h"
+#import "FIRStorageUpdateMetadataTask.h"
+#import "FIRStorageUploadTask_Private.h"
+#import "FIRStorageUtils.h"
+#import "FIRStorage_Private.h"
+
+#import "FIRApp.h"
+#import "FIROptions.h"
+
+#import <GTMSessionFetcher/GTMSessionFetcher.h>
+#import "GTMSessionFetcherService.h"
+
+@implementation FIRStorageReference
+
+- (instancetype)init {
+ FIRStorage *storage = [FIRStorage storage];
+ NSString *storageBucket = storage.app.options.storageBucket;
+ FIRStoragePath *path = [[FIRStoragePath alloc] initWithBucket:storageBucket object:nil];
+ FIRStorageReference *reference = [self initWithStorage:storage path:path];
+ return reference;
+}
+
+- (instancetype)initWithStorage:(FIRStorage *)storage path:(FIRStoragePath *)path {
+ self = [super init];
+ if (self) {
+ _storage = storage;
+ _path = path;
+ }
+ return self;
+}
+
+#pragma mark - NSObject overrides
+
+- (instancetype)copyWithZone:(NSZone *)zone {
+ FIRStorageReference *copiedReference =
+ [[[self class] allocWithZone:zone] initWithStorage:_storage path:_path];
+ return copiedReference;
+}
+
+- (BOOL)isEqual:(id)object {
+ if (self == object) {
+ return YES;
+ }
+
+ if (![object isKindOfClass:[FIRStorageReference class]]) {
+ return NO;
+ }
+
+ BOOL isObjectEqual = [self isEqualToFIRStorageReference:(FIRStorageReference *)object];
+ return isObjectEqual;
+}
+
+- (BOOL)isEqualToFIRStorageReference:(FIRStorageReference *)reference {
+ BOOL isEqual = [_storage isEqual:reference.storage] && [_path isEqual:reference.path];
+ return isEqual;
+}
+
+- (NSUInteger)hash {
+ NSUInteger hash = [_storage hash] ^ [_path hash];
+ return hash;
+}
+
+- (NSString *)description {
+ return [self stringValue];
+}
+
+- (NSString *)stringValue {
+ NSString *value = [NSString stringWithFormat:@"gs://%@/%@", _path.bucket, _path.object ?: @""];
+ return value;
+}
+
+#pragma mark - Property Getters
+
+- (NSString *)bucket {
+ NSString *bucket = _path.bucket;
+ return bucket;
+}
+
+- (NSString *)fullPath {
+ NSString *path = _path.object;
+ if (!path) {
+ path = @"";
+ }
+ return path;
+}
+
+- (NSString *)name {
+ NSString *name = [_path.object lastPathComponent];
+ if (!name) {
+ name = @"";
+ }
+ return name;
+}
+
+#pragma mark - Path Operations
+
+- (FIRStorageReference *)root {
+ FIRStoragePath *rootPath = [_path root];
+ FIRStorageReference *rootReference =
+ [[FIRStorageReference alloc] initWithStorage:_storage path:rootPath];
+ return rootReference;
+}
+
+- (nullable FIRStorageReference *)parent {
+ FIRStoragePath *parentPath = [_path parent];
+ if (!parentPath) {
+ return nil;
+ }
+
+ FIRStorageReference *parentReference =
+ [[FIRStorageReference alloc] initWithStorage:_storage path:parentPath];
+ return parentReference;
+}
+
+- (FIRStorageReference *)child:(NSString *)path {
+ FIRStoragePath *childPath = [_path child:path];
+ FIRStorageReference *childReference =
+ [[FIRStorageReference alloc] initWithStorage:_storage path:childPath];
+ return childReference;
+}
+
+#pragma mark - Uploads
+
+- (FIRStorageUploadTask *)putData:(NSData *)uploadData {
+ return [self putData:uploadData metadata:nil completion:nil];
+}
+
+- (FIRStorageUploadTask *)putData:(NSData *)uploadData
+ metadata:(nullable FIRStorageMetadata *)metadata {
+ return [self putData:uploadData metadata:metadata completion:nil];
+}
+
+- (FIRStorageUploadTask *)putData:(NSData *)uploadData
+ metadata:(nullable FIRStorageMetadata *)metadata
+ completion:(nullable FIRStorageVoidMetadataError)completion {
+ if (!metadata) {
+ metadata = [[FIRStorageMetadata alloc] init];
+ }
+
+ metadata.path = _path.object;
+ metadata.name = [_path.object lastPathComponent];
+ FIRStorageUploadTask *task =
+ [[FIRStorageUploadTask alloc] initWithReference:self
+ fetcherService:_storage.fetcherServiceForApp
+ data:uploadData
+ metadata:metadata];
+
+ if (completion) {
+ dispatch_queue_t callbackQueue = _storage.fetcherServiceForApp.callbackQueue;
+ if (!callbackQueue) {
+ callbackQueue = dispatch_get_main_queue();
+ }
+
+ [task observeStatus:FIRStorageTaskStatusSuccess
+ handler:^(FIRStorageTaskSnapshot *_Nonnull snapshot) {
+ dispatch_async(callbackQueue, ^{
+ completion(snapshot.metadata, nil);
+ });
+ }];
+ [task observeStatus:FIRStorageTaskStatusFailure
+ handler:^(FIRStorageTaskSnapshot *_Nonnull snapshot) {
+ dispatch_async(callbackQueue, ^{
+ completion(nil, snapshot.error);
+ });
+ }];
+ }
+ [task enqueue];
+ return task;
+}
+
+- (FIRStorageUploadTask *)putFile:(NSURL *)fileURL {
+ return [self putFile:fileURL metadata:nil completion:nil];
+}
+
+- (FIRStorageUploadTask *)putFile:(NSURL *)fileURL
+ metadata:(nullable FIRStorageMetadata *)metadata {
+ return [self putFile:fileURL metadata:metadata completion:nil];
+}
+
+- (FIRStorageUploadTask *)putFile:(NSURL *)fileURL
+ metadata:(nullable FIRStorageMetadata *)metadata
+ completion:(nullable FIRStorageVoidMetadataError)completion {
+ if (!metadata) {
+ metadata = [[FIRStorageMetadata alloc] init];
+ }
+
+ metadata.path = _path.object;
+ metadata.name = [_path.object lastPathComponent];
+ FIRStorageUploadTask *task =
+ [[FIRStorageUploadTask alloc] initWithReference:self
+ fetcherService:_storage.fetcherServiceForApp
+ file:fileURL
+ metadata:metadata];
+
+ if (completion) {
+ dispatch_queue_t callbackQueue = _storage.fetcherServiceForApp.callbackQueue;
+ if (!callbackQueue) {
+ callbackQueue = dispatch_get_main_queue();
+ }
+
+ [task observeStatus:FIRStorageTaskStatusSuccess
+ handler:^(FIRStorageTaskSnapshot *_Nonnull snapshot) {
+ dispatch_async(callbackQueue, ^{
+ completion(snapshot.metadata, nil);
+ });
+ }];
+ [task observeStatus:FIRStorageTaskStatusFailure
+ handler:^(FIRStorageTaskSnapshot *_Nonnull snapshot) {
+ dispatch_async(callbackQueue, ^{
+ completion(nil, snapshot.error);
+ });
+ }];
+ }
+ [task enqueue];
+ return task;
+}
+
+#pragma mark - Downloads
+
+- (FIRStorageDownloadTask *)dataWithMaxSize:(int64_t)size
+ completion:(FIRStorageVoidDataError)completion {
+ FIRStorageDownloadTask *task =
+ [[FIRStorageDownloadTask alloc] initWithReference:self
+ fetcherService:_storage.fetcherServiceForApp
+ file:nil];
+
+ dispatch_queue_t callbackQueue = _storage.fetcherServiceForApp.callbackQueue;
+ if (!callbackQueue) {
+ callbackQueue = dispatch_get_main_queue();
+ }
+
+ [task observeStatus:FIRStorageTaskStatusSuccess
+ handler:^(FIRStorageTaskSnapshot *_Nonnull snapshot) {
+ FIRStorageDownloadTask *task = snapshot.task;
+ dispatch_async(callbackQueue, ^{
+ completion(task.downloadData, nil);
+ });
+ }];
+ [task observeStatus:FIRStorageTaskStatusFailure
+ handler:^(FIRStorageTaskSnapshot *_Nonnull snapshot) {
+ dispatch_async(callbackQueue, ^{
+ completion(nil, snapshot.error);
+ });
+ }];
+ [task observeStatus:FIRStorageTaskStatusProgress
+ handler:^(FIRStorageTaskSnapshot *_Nonnull snapshot) {
+ FIRStorageDownloadTask *task = snapshot.task;
+ if (task.progress.totalUnitCount > size ||
+ task.progress.completedUnitCount > size) {
+ NSDictionary *infoDictionary = @{
+ @"totalSize" : @(task.progress.totalUnitCount),
+ @"maxAllowedSize" : @(size)
+ };
+ NSError *error =
+ [FIRStorageErrors errorWithCode:FIRStorageErrorCodeDownloadSizeExceeded
+ infoDictionary:infoDictionary];
+ [task cancelWithError:error];
+ }
+ }];
+ [task enqueue];
+ return task;
+}
+
+- (FIRStorageDownloadTask *)writeToFile:(NSURL *)fileURL {
+ return [self writeToFile:fileURL completion:nil];
+}
+
+- (FIRStorageDownloadTask *)writeToFile:(NSURL *)fileURL
+ completion:(FIRStorageVoidURLError)completion {
+ FIRStorageDownloadTask *task =
+ [[FIRStorageDownloadTask alloc] initWithReference:self
+ fetcherService:_storage.fetcherServiceForApp
+ file:fileURL];
+ if (completion) {
+ dispatch_queue_t callbackQueue = _storage.fetcherServiceForApp.callbackQueue;
+ if (!callbackQueue) {
+ callbackQueue = dispatch_get_main_queue();
+ }
+
+ [task observeStatus:FIRStorageTaskStatusSuccess
+ handler:^(FIRStorageTaskSnapshot *_Nonnull snapshot) {
+ dispatch_async(callbackQueue, ^{
+ completion(fileURL, nil);
+ });
+ }];
+ [task observeStatus:FIRStorageTaskStatusFailure
+ handler:^(FIRStorageTaskSnapshot *_Nonnull snapshot) {
+ dispatch_async(callbackQueue, ^{
+ completion(nil, snapshot.error);
+ });
+ }];
+ }
+ [task enqueue];
+ return task;
+}
+
+- (void)downloadURLWithCompletion:(FIRStorageVoidURLError)completion {
+ dispatch_queue_t callbackQueue = _storage.fetcherServiceForApp.callbackQueue;
+ if (!callbackQueue) {
+ callbackQueue = dispatch_get_main_queue();
+ }
+
+ return [self metadataWithCompletion:^(FIRStorageMetadata *metadata, NSError *error) {
+ dispatch_async(callbackQueue, ^{
+ completion(metadata.downloadURL, error);
+ });
+ }];
+}
+
+#pragma mark - Metadata Operations
+
+- (void)metadataWithCompletion:(FIRStorageVoidMetadataError)completion {
+ FIRStorageGetMetadataTask *task =
+ [[FIRStorageGetMetadataTask alloc] initWithReference:self
+ fetcherService:_storage.fetcherServiceForApp
+ completion:completion];
+ [task enqueue];
+}
+
+- (void)updateMetadata:(FIRStorageMetadata *)metadata
+ completion:(nullable FIRStorageVoidMetadataError)completion {
+ FIRStorageUpdateMetadataTask *task =
+ [[FIRStorageUpdateMetadataTask alloc] initWithReference:self
+ fetcherService:_storage.fetcherServiceForApp
+ metadata:metadata
+ completion:completion];
+ [task enqueue];
+}
+
+#pragma mark - Delete
+
+- (void)deleteWithCompletion:(nullable FIRStorageVoidError)completion {
+ FIRStorageDeleteTask *task =
+ [[FIRStorageDeleteTask alloc] initWithReference:self
+ fetcherService:_storage.fetcherServiceForApp
+ completion:completion];
+ [task enqueue];
+}
+
+@end
diff --git a/Firebase/Storage/FIRStorageSwiftNameSupport.h b/Firebase/Storage/FIRStorageSwiftNameSupport.h
new file mode 100644
index 0000000..529adf4
--- /dev/null
+++ b/Firebase/Storage/FIRStorageSwiftNameSupport.h
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef FIR_SWIFT_NAME
+
+#import <Foundation/Foundation.h>
+
+// NS_SWIFT_NAME can only translate factory methods before the iOS 9.3 SDK.
+// // Wrap it in our own macro if it's a non-compatible SDK.
+#ifdef __IPHONE_9_3
+#define FIR_SWIFT_NAME(X) NS_SWIFT_NAME(X)
+#else
+#define FIR_SWIFT_NAME(X) // Intentionally blank.
+#endif // #ifdef __IPHONE_9_3
+
+#endif // FIR_SWIFT_NAME \ No newline at end of file
diff --git a/Firebase/Storage/FIRStorageTask.h b/Firebase/Storage/FIRStorageTask.h
new file mode 100644
index 0000000..0428220
--- /dev/null
+++ b/Firebase/Storage/FIRStorageTask.h
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+#import "FIRStorageConstants.h"
+#import "FIRStorageMetadata.h"
+#import "FIRStorageSwiftNameSupport.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * A superclass to all FIRStorage*Tasks, including FIRStorageUploadTask
+ * and FIRStorageDownloadTask, to provide state transitions, event raising, and common storage
+ * or metadata and errors.
+ * Callbacks are always fired on the developer specified callback queue.
+ * If no queue is specified by the developer, it defaults to the main queue.
+ * Currently not thread safe, so only call methods on the main thread.
+ */
+FIR_SWIFT_NAME(StorageTask)
+@interface FIRStorageTask : NSObject
+
+/**
+ * An immutable view of the task and associated metadata, progress, error, etc.
+ */
+@property(strong, readonly, nonatomic, nonnull) FIRStorageTaskSnapshot *snapshot;
+
+@end
+
+/**
+ * Defines task operations such as pause, resume, cancel, and enqueue for all tasks.
+ * All tasks are required to implement enqueue, which begins the task, and may optionally
+ * implement pause, resume, and cancel, which operate on the task to pause, resume, and cancel
+ * operations.
+ */
+FIR_SWIFT_NAME(StorageTaskManagement)
+@protocol FIRStorageTaskManagement<NSObject>
+
+@required
+/**
+ * Prepares a task and begins execution.
+ */
+- (void)enqueue;
+
+@optional
+/**
+ * Pauses a task currently in progress.
+ */
+- (void)pause;
+
+/**
+ * Cancels a task currently in progress.
+ */
+- (void)cancel;
+
+/**
+ * Resumes a task that is paused.
+ */
+- (void)resume;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Storage/FIRStorageTask.m b/Firebase/Storage/FIRStorageTask.m
new file mode 100644
index 0000000..3c1cf6f
--- /dev/null
+++ b/Firebase/Storage/FIRStorageTask.m
@@ -0,0 +1,65 @@
+// Copyright 2017 Google
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "FIRStorageTask.h"
+
+#import "FIRStorage.h"
+#import "FIRStorage_Private.h"
+#import "FIRStorageReference.h"
+#import "FIRStorageReference_Private.h"
+#import "FIRStorageTaskSnapshot.h"
+#import "FIRStorageTaskSnapshot_Private.h"
+#import "FIRStorageTask_Private.h"
+
+#import "GTMSessionFetcherService.h"
+
+@implementation FIRStorageTask
+
+- (instancetype)init {
+ FIRStorage *storage = [FIRStorage storage];
+ FIRStorageReference *reference = [storage reference];
+ FIRStorageTask *task =
+ [self initWithReference:reference fetcherService:storage.fetcherServiceForApp];
+ return task;
+}
+
+- (instancetype)initWithReference:(FIRStorageReference *)reference
+ fetcherService:(GTMSessionFetcherService *)service {
+ self = [super init];
+ if (self) {
+ _reference = reference;
+ _baseRequest = [FIRStorageUtils defaultRequestForPath:reference.path];
+ _fetcherService = service;
+ _fetcherService.maxRetryInterval = _reference.storage.maxOperationRetryTime;
+ }
+ return self;
+}
+
+- (FIRStorageTaskSnapshot *)snapshot {
+ @synchronized(self) {
+ NSProgress *progress =
+ [NSProgress progressWithTotalUnitCount:self.progress.totalUnitCount];
+ progress.completedUnitCount = self.progress.completedUnitCount;
+ FIRStorageTaskSnapshot *snapshot =
+ [[FIRStorageTaskSnapshot alloc] initWithTask:self
+ state:self.state
+ metadata:self.metadata
+ reference:self.reference
+ progress:progress
+ error:[self.error copy]];
+ return snapshot;
+ }
+}
+
+@end
diff --git a/Firebase/Storage/FIRStorageTaskSnapshot.h b/Firebase/Storage/FIRStorageTaskSnapshot.h
new file mode 100644
index 0000000..b654c09
--- /dev/null
+++ b/Firebase/Storage/FIRStorageTaskSnapshot.h
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+#import "FIRStorageConstants.h"
+#import "FIRStorageSwiftNameSupport.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@class FIRStorageMetadata;
+@class FIRStorageReference;
+@class FIRStorageTask;
+
+/**
+ * FIRStorageTaskSnapshot represents an immutable view of a task.
+ * A Snapshot contains a task, storage reference, metadata (if it exists),
+ * progress, and an error (if one occurred).
+ */
+FIR_SWIFT_NAME(StorageTaskSnapshot)
+@interface FIRStorageTaskSnapshot : NSObject
+
+/**
+ * Subclass of FIRStorageTask this snapshot represents.
+ */
+@property(readonly, copy, nonatomic) __kindof FIRStorageTask *task;
+
+/**
+ * Metadata returned by the task, or nil if no metadata returned.
+ */
+@property(readonly, copy, nonatomic, nullable) FIRStorageMetadata *metadata;
+
+/**
+ * FIRStorageReference this task is operates on.
+ */
+@property(readonly, copy, nonatomic) FIRStorageReference *reference;
+
+/**
+ * NSProgress object which tracks the progess of an upload or download.
+ */
+@property(readonly, strong, nonatomic, nullable) NSProgress *progress;
+
+/**
+ * Error during task execution, or nil if no error occurred.
+ */
+@property(readonly, copy, nonatomic, nullable) NSError *error;
+
+/**
+ * Status of the task.
+ */
+@property(readonly, nonatomic) FIRStorageTaskStatus status;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Storage/FIRStorageTaskSnapshot.m b/Firebase/Storage/FIRStorageTaskSnapshot.m
new file mode 100644
index 0000000..050d05c
--- /dev/null
+++ b/Firebase/Storage/FIRStorageTaskSnapshot.m
@@ -0,0 +1,88 @@
+// Copyright 2017 Google
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "FIRStorageTaskSnapshot.h"
+#import "FIRStorageTaskSnapshot_Private.h"
+
+#import "FIRStorageTask_Private.h"
+
+@implementation FIRStorageTaskSnapshot
+
+- (instancetype)initWithTask:(__kindof FIRStorageTask *)task
+ state:(FIRStorageTaskState)state
+ metadata:(nullable FIRStorageMetadata *)metadata
+ reference:(FIRStorageReference *)reference
+ progress:(nullable NSProgress *)progress
+ error:(nullable NSError *)error {
+ self = [super init];
+ if (self) {
+ _task = task;
+ _metadata = metadata;
+ _reference = reference;
+ _progress = progress;
+ _error = error;
+
+ switch (state) {
+ case FIRStorageTaskStateQueueing:
+ case FIRStorageTaskStateRunning:
+ case FIRStorageTaskStateResuming:
+ _status = FIRStorageTaskStatusResume;
+ break;
+
+ case FIRStorageTaskStateProgress:
+ _status = FIRStorageTaskStatusProgress;
+ break;
+
+ case FIRStorageTaskStatePaused:
+ case FIRStorageTaskStatePausing:
+ _status = FIRStorageTaskStatusPause;
+ break;
+
+ case FIRStorageTaskStateSuccess:
+ case FIRStorageTaskStateCompleting:
+ _status = FIRStorageTaskStatusSuccess;
+ break;
+
+ case FIRStorageTaskStateCancelled:
+ case FIRStorageTaskStateFailing:
+ case FIRStorageTaskStateFailed:
+ _status = FIRStorageTaskStatusFailure;
+ break;
+
+ default:
+ _status = FIRStorageTaskStatusUnknown;
+ }
+ }
+ return self;
+}
+
+
+-(NSString *)description {
+ switch (_status) {
+ case FIRStorageTaskStatusResume:
+ return @"<State: Resume>";
+ case FIRStorageTaskStatusProgress:
+ return [NSString stringWithFormat:@"<State: Progress, Progress: %@>", _progress];
+ case FIRStorageTaskStatusPause:
+ return @"<State: Paused>";
+ case FIRStorageTaskStatusSuccess:
+ return @"<State: Success>";
+ case FIRStorageTaskStatusFailure:
+ return [NSString stringWithFormat:@"<State: Failed, Error: %@>", _error];
+ default:
+ return @"<State: Unknown>";
+ };
+}
+
+@end
diff --git a/Firebase/Storage/FIRStorageTokenAuthorizer.m b/Firebase/Storage/FIRStorageTokenAuthorizer.m
new file mode 100644
index 0000000..36b94a9
--- /dev/null
+++ b/Firebase/Storage/FIRStorageTokenAuthorizer.m
@@ -0,0 +1,131 @@
+// Copyright 2017 Google
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "../../Firebase/Core/Private/FIRAppInternal.h"
+
+#import "FIRStorageTokenAuthorizer.h"
+
+#import "FIRStorageConstants.h"
+#import "FIRStorageConstants_Private.h"
+#import "FIRStorageErrors.h"
+
+#import "FirebaseStorage.h"
+
+#import "FIRApp.h"
+#import "FIROptions.h"
+
+@implementation FIRStorageTokenAuthorizer {
+ @private
+ // Firebase App which vends tokens
+ FIRApp *_app;
+}
+
+@synthesize fetcherService = _fetcherService;
+
+- (instancetype)initWithApp:(FIRApp *)app fetcherService:(GTMSessionFetcherService *)service {
+ self = [super init];
+ if (self) {
+ _app = app;
+ _fetcherService = service;
+ }
+ return self;
+}
+
+#pragma mark - GTMFetcherAuthorizationProtocol methods
+
+- (void)authorizeRequest:(NSMutableURLRequest *)request
+ delegate:(id)delegate
+ didFinishSelector:(SEL)sel {
+ // Set version header on each request
+ NSString *versionString = [NSString stringWithFormat:@"ios/%s", FIRStorageVersionString];
+ [request setValue:versionString forHTTPHeaderField:@"x-firebase-storage-version"];
+
+ // Set GMP ID on each request
+ NSString *GMPAppId = _app.options.googleAppID;
+ [request setValue:GMPAppId forHTTPHeaderField:@"x-firebase-gmpid"];
+
+ if (delegate && sel) {
+ id selfParam = self;
+ NSMethodSignature *sig = [delegate methodSignatureForSelector:sel];
+ NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig];
+ [invocation setSelector:sel];
+ [invocation setTarget:delegate];
+ [invocation setArgument:&selfParam atIndex:2];
+ [invocation setArgument:&request atIndex:3];
+
+ dispatch_queue_t callbackQueue = self.fetcherService.callbackQueue;
+ if (!callbackQueue) {
+ callbackQueue = dispatch_get_main_queue();
+ }
+
+ [invocation retainArguments];
+ if (_app.getTokenImplementation) {
+ [_app getTokenForcingRefresh:NO
+ withCallback:^(NSString *_Nullable token, NSError *_Nullable error) {
+ if (error) {
+ NSMutableDictionary *errorDictionary =
+ [NSMutableDictionary dictionaryWithDictionary:error.userInfo];
+ errorDictionary[kFIRStorageResponseErrorDomain] = error.domain;
+ errorDictionary[kFIRStorageResponseErrorCode] = @(error.code);
+
+ NSError *tokenError =
+ [FIRStorageErrors errorWithCode:FIRStorageErrorCodeUnauthenticated
+ infoDictionary:errorDictionary];
+ [invocation setArgument:&tokenError atIndex:4];
+ } else if (token) {
+ NSString *firebaseToken =
+ [NSString stringWithFormat:kFIRStorageAuthTokenFormat, token];
+ [request setValue:firebaseToken forHTTPHeaderField:@"Authorization"];
+ }
+ dispatch_async(callbackQueue, ^{
+ [invocation invoke];
+ });
+ }];
+ } else {
+ dispatch_async(callbackQueue, ^{
+ [invocation invoke];
+ });
+ }
+ }
+}
+
+// Note that stopAuthorization, isAuthorizingRequest, and userEmail
+// aren't relevant with the Firebase App/Auth implementation of tokens,
+// and thus aren't implemented. Token refresh is handled transparently
+// for us, and we don't allow the auth request to be stopped.
+// Auth is also not required so the world doesn't stop.
+- (void)stopAuthorization {
+ // Noop
+}
+
+- (void)stopAuthorizationForRequest:(NSURLRequest *)request {
+ // Noop
+}
+
+- (BOOL)isAuthorizingRequest:(NSURLRequest *)request {
+ return NO;
+}
+
+- (BOOL)isAuthorizedRequest:(NSURLRequest *)request {
+ NSString *authHeader = request.allHTTPHeaderFields[@"Authorization"];
+ BOOL isFirebaseToken = [authHeader hasPrefix:@"Firebase"];
+ return isFirebaseToken;
+}
+
+- (NSString *)userEmail {
+ // Noop
+ return nil;
+}
+
+@end
diff --git a/Firebase/Storage/FIRStorageUpdateMetadataTask.m b/Firebase/Storage/FIRStorageUpdateMetadataTask.m
new file mode 100644
index 0000000..dbd276b
--- /dev/null
+++ b/Firebase/Storage/FIRStorageUpdateMetadataTask.m
@@ -0,0 +1,91 @@
+// Copyright 2017 Google
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "FIRStorageUpdateMetadataTask.h"
+
+#import "FIRStorageMetadata_Private.h"
+#import "FIRStorageTask_Private.h"
+
+@implementation FIRStorageUpdateMetadataTask {
+ @private
+ FIRStorageVoidMetadataError _completion;
+ // Metadata used in the update request
+ FIRStorageMetadata *_updateMetadata;
+}
+
+- (instancetype)initWithReference:(FIRStorageReference *)reference
+ fetcherService:(GTMSessionFetcherService *)service
+ metadata:(FIRStorageMetadata *)metadata
+ completion:(FIRStorageVoidMetadataError)completion {
+ self = [super initWithReference:reference fetcherService:service];
+ if (self) {
+ _updateMetadata = [metadata copy];
+ _completion = [completion copy];
+ }
+ return self;
+}
+
+- (void)enqueue {
+ NSMutableURLRequest *request = [self.baseRequest mutableCopy];
+ NSDictionary *updateDictionary = [_updateMetadata dictionaryRepresentation];
+ NSData *updateData = [NSData frs_dataFromJSONDictionary:updateDictionary];
+ request.HTTPMethod = @"PATCH";
+ request.timeoutInterval = self.reference.storage.maxUploadRetryTime;
+ request.HTTPBody = updateData;
+ NSString *typeString = @"application/json; charset=UTF-8";
+ [request setValue:typeString forHTTPHeaderField:@"Content-Type"];
+ NSString *lengthString = [NSString stringWithFormat:@"%zu", (unsigned long)[updateData length]];
+ [request setValue:lengthString forHTTPHeaderField:@"Content-Length"];
+
+ FIRStorageVoidMetadataError callback = _completion;
+ _completion = nil;
+
+ GTMSessionFetcher *fetcher = [self.fetcherService fetcherWithRequest:request];
+ fetcher.comment = @"UpdateMetadataTask";
+ [fetcher beginFetchWithCompletionHandler:^(NSData *data, NSError *error) {
+ if (error) {
+ if (!self.error) {
+ self.error = [FIRStorageErrors errorWithServerError:error reference:self.reference];
+ }
+ if (callback) {
+ callback(nil, self.error);
+ }
+ return;
+ }
+
+ NSDictionary *responseDictionary = [NSDictionary frs_dictionaryFromJSONData:data];
+ if (responseDictionary) {
+ FIRStorageMetadata *metadata =
+ [[FIRStorageMetadata alloc] initWithDictionary:responseDictionary];
+ [metadata setType:FIRStorageMetadataTypeFile];
+ if (callback){
+ callback(metadata, nil);
+ }
+ } else {
+ NSString *returnedData = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
+ NSString *invalidDataString =
+ [NSString stringWithFormat:kFIRStorageInvalidDataFormat, returnedData];
+ NSDictionary *dict;
+ if (invalidDataString.length > 0) {
+ dict = @{NSLocalizedFailureReasonErrorKey : invalidDataString};
+ }
+ self.error = [FIRStorageErrors errorWithCode:FIRStorageErrorCodeUnknown infoDictionary:dict];
+ if (callback) {
+ callback(nil, self.error);
+ }
+ }
+ }];
+}
+
+@end
diff --git a/Firebase/Storage/FIRStorageUploadTask.h b/Firebase/Storage/FIRStorageUploadTask.h
new file mode 100644
index 0000000..cdf1d29
--- /dev/null
+++ b/Firebase/Storage/FIRStorageUploadTask.h
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+#import "FIRStorageObservableTask.h"
+#import "FIRStorageSwiftNameSupport.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * FIRStorageUploadTask implements resumable uploads to a file in Firebase Storage.
+ * Uploads can be returned on completion with a completion callback, and can be monitored
+ * by attaching observers, or controlled by calling FIRStorageTask#pause, FIRStorageTask#resume,
+ * or FIRStorageTask#cancel.
+ * Uploads can take NSData in memory, or an NSURL to a file on disk.
+ * Uploads are performed on a background queue, and callbacks are raised on the developer
+ * specified callbackQueue in FIRStorage, or the main queue if left unspecified.
+ * Currently all uploads must be initiated and managed on the main queue.
+ */
+FIR_SWIFT_NAME(StorageUploadTask)
+@interface FIRStorageUploadTask : FIRStorageObservableTask<FIRStorageTaskManagement>
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Storage/FIRStorageUploadTask.m b/Firebase/Storage/FIRStorageUploadTask.m
new file mode 100644
index 0000000..74741b0
--- /dev/null
+++ b/Firebase/Storage/FIRStorageUploadTask.m
@@ -0,0 +1,199 @@
+// Copyright 2017 Google
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "FIRStorageUploadTask.h"
+
+#import "FIRStorageConstants_Private.h"
+#import "FIRStorageMetadata_Private.h"
+#import "FIRStorageObservableTask_Private.h"
+#import "FIRStorageTask_Private.h"
+#import "FIRStorageUploadTask_Private.h"
+
+#import "GTMSessionUploadFetcher.h"
+
+@implementation FIRStorageUploadTask
+
+@synthesize progress = _progress;
+
+- (instancetype)initWithReference:(FIRStorageReference *)reference
+ fetcherService:(GTMSessionFetcherService *)service
+ data:(NSData *)uploadData
+ metadata:(FIRStorageMetadata *)metadata {
+ self = [super initWithReference:reference fetcherService:service];
+ if (self) {
+ _uploadMetadata = [metadata copy];
+ _uploadData = [uploadData copy];
+ _progress = [NSProgress progressWithTotalUnitCount:[_uploadData length]];
+
+ if (!_uploadMetadata.contentType) {
+ _uploadMetadata.contentType = @"application/octet-stream";
+ }
+ }
+ return self;
+}
+
+- (instancetype)initWithReference:(FIRStorageReference *)reference
+ fetcherService:(GTMSessionFetcherService *)service
+ file:(NSURL *)fileURL
+ metadata:(FIRStorageMetadata *)metadata {
+ self = [super initWithReference:reference fetcherService:service];
+ if (self) {
+ _uploadMetadata = [metadata copy];
+ _fileURL = [fileURL copy];
+ _progress = [NSProgress progressWithTotalUnitCount:0];
+
+ NSString *mimeType = [FIRStorageUtils MIMETypeForExtension:[_fileURL pathExtension]];
+
+ if (!_uploadMetadata.contentType) {
+ _uploadMetadata.contentType = mimeType ?: @"application/octet-stream";
+ }
+ }
+ return self;
+}
+
+- (void)enqueue {
+ NSAssert([NSThread isMainThread], @"Upload attempting to execute on non main queue! Please only "
+ @"execute this method on the main queue.");
+ self.state = FIRStorageTaskStateQueueing;
+
+ NSMutableURLRequest *request = [self.baseRequest mutableCopy];
+ request.HTTPMethod = @"POST";
+ request.timeoutInterval = self.reference.storage.maxUploadRetryTime;
+ NSData *bodyData = [NSData frs_dataFromJSONDictionary:[_uploadMetadata dictionaryRepresentation]];
+ request.HTTPBody = bodyData;
+ [request setValue:@"application/json; charset=UTF-8" forHTTPHeaderField:@"Content-Type"];
+ NSString *contentLengthString = [NSString stringWithFormat:@"%zu",
+ (unsigned long)[bodyData length]];
+ [request setValue:contentLengthString forHTTPHeaderField:@"Content-Length"];
+
+ NSURLComponents *components =
+ [NSURLComponents componentsWithURL:request.URL resolvingAgainstBaseURL:NO];
+
+ if ([components.host isEqual:kGCSHost]) {
+ [components setPercentEncodedPath:[@"/upload" stringByAppendingString:components.path]];
+ }
+
+ NSDictionary *queryParams = @{ @"uploadType" : @"resumable", @"name" : self.uploadMetadata.path };
+ [components setPercentEncodedQuery:[FIRStorageUtils queryStringForDictionary:queryParams]];
+ request.URL = components.URL;
+
+ GTMSessionUploadFetcher *uploadFetcher =
+ [GTMSessionUploadFetcher uploadFetcherWithRequest:request
+ uploadMIMEType:_uploadMetadata.contentType
+ chunkSize:kGTMSessionUploadFetcherStandardChunkSize
+ fetcherService:self.fetcherService];
+
+ if (_uploadData) {
+ [uploadFetcher setUploadData:_uploadData];
+ uploadFetcher.comment = @"Data UploadTask";
+ } else if (_fileURL) {
+ [uploadFetcher setUploadFileURL:_fileURL];
+ uploadFetcher.comment = @"File UploadTask";
+ }
+
+ uploadFetcher.maxRetryInterval = self.reference.storage.maxUploadRetryTime;
+
+ [uploadFetcher setSendProgressBlock:^(int64_t bytesSent, int64_t totalBytesSent,
+ int64_t totalBytesExpectedToSend) {
+ self.state = FIRStorageTaskStateProgress;
+ self.progress.completedUnitCount = totalBytesSent;
+ self.progress.totalUnitCount = totalBytesExpectedToSend;
+ self.metadata = _uploadMetadata;
+ [self fireHandlersForStatus:FIRStorageTaskStatusProgress snapshot:self.snapshot];
+ self.state = FIRStorageTaskStateRunning;
+ }];
+
+ _uploadFetcher = uploadFetcher;
+
+ // Process fetches
+ self.state = FIRStorageTaskStateRunning;
+ [_uploadFetcher beginFetchWithCompletionHandler:^(NSData *_Nullable data,
+ NSError *_Nullable error) {
+ // Fire last progress updates
+ [self fireHandlersForStatus:FIRStorageTaskStatusProgress snapshot:self.snapshot];
+
+ // Handle potential issues with upload
+ if (error) {
+ self.state = FIRStorageTaskStateFailed;
+ self.error = [FIRStorageErrors errorWithServerError:error reference:self.reference];
+ self.metadata = _uploadMetadata;
+ [self fireHandlersForStatus:FIRStorageTaskStatusFailure snapshot:self.snapshot];
+ [self removeAllObservers];
+ return;
+ }
+
+ // Upload completed successfully, fire completion callbacks
+ self.state = FIRStorageTaskStateSuccess;
+
+ NSDictionary *responseDictionary = [NSDictionary frs_dictionaryFromJSONData:data];
+ if (responseDictionary) {
+ FIRStorageMetadata *metadata =
+ [[FIRStorageMetadata alloc] initWithDictionary:responseDictionary];
+ [metadata setType:FIRStorageMetadataTypeFile];
+ self.metadata = metadata;
+ } else {
+ NSString *returnedData = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
+ NSString *invalidDataString =
+ [NSString stringWithFormat:kFIRStorageInvalidDataFormat, returnedData];
+ NSDictionary *dict;
+ if (invalidDataString.length > 0) {
+ dict = @{NSLocalizedFailureReasonErrorKey : invalidDataString};
+ }
+ self.error =
+ [FIRStorageErrors errorWithCode:FIRStorageErrorCodeUnknown infoDictionary:dict];
+ }
+
+ [self fireHandlersForStatus:FIRStorageTaskStatusSuccess snapshot:self.snapshot];
+ [self removeAllObservers];
+ }];
+}
+
+#pragma mark - Upload Management
+
+- (void)cancel {
+ NSAssert([NSThread isMainThread], @"Cancel attempting to execute on non main queue! Please only "
+ @"execute this method on the main queue.");
+ self.state = FIRStorageTaskStateCancelled;
+ [_uploadFetcher stopFetching];
+ if (self.state != FIRStorageTaskStateSuccess) {
+ self.metadata = _uploadMetadata;
+ }
+ self.error = [FIRStorageErrors errorWithCode:FIRStorageErrorCodeCancelled];
+ [self fireHandlersForStatus:FIRStorageTaskStatusFailure snapshot:self.snapshot];
+}
+
+- (void)pause {
+ NSAssert([NSThread isMainThread], @"Pause attempting to execute on non main queue! Please only "
+ @"execute this method on the main queue.");
+ self.state = FIRStorageTaskStatePaused;
+ [_uploadFetcher pauseFetching];
+ if (self.state != FIRStorageTaskStateSuccess) {
+ self.metadata = _uploadMetadata;
+ }
+ [self fireHandlersForStatus:FIRStorageTaskStatusPause snapshot:self.snapshot];
+}
+
+- (void)resume {
+ NSAssert([NSThread isMainThread], @"Resume attempting to execute on non main queue! Please only "
+ @"execute this method on the main queue.");
+ self.state = FIRStorageTaskStateResuming;
+ [_uploadFetcher resumeFetching];
+ if (self.state != FIRStorageTaskStateSuccess) {
+ self.metadata = _uploadMetadata;
+ }
+ [self fireHandlersForStatus:FIRStorageTaskStatusResume snapshot:self.snapshot];
+ self.state = FIRStorageTaskStateRunning;
+}
+
+@end
diff --git a/Firebase/Storage/FIRStorageUtils.m b/Firebase/Storage/FIRStorageUtils.m
new file mode 100644
index 0000000..e0abe0a
--- /dev/null
+++ b/Firebase/Storage/FIRStorageUtils.m
@@ -0,0 +1,121 @@
+// Copyright 2017 Google
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <MobileCoreServices/MobileCoreServices.h>
+
+#import "FIRStorageUtils.h"
+
+#import "FIRStorageConstants_Private.h"
+#import "FIRStoragePath.h"
+
+#import "FirebaseStorage.h"
+
+#import "GTMSessionFetcher.h"
+
+// This is the list at https://cloud.google.com/storage/docs/json_api/ without & and +.
+NSString *const kGCSObjectAllowedCharacterSet =
+ @"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~!$'()*,;=:@";
+
+@implementation FIRStorageUtils
+
++ (nullable NSString *)GCSEscapedString:(NSString *)string {
+ NSCharacterSet *allowedCharacters =
+ [NSCharacterSet characterSetWithCharactersInString:kGCSObjectAllowedCharacterSet];
+
+ return [string stringByAddingPercentEncodingWithAllowedCharacters:allowedCharacters];
+}
+
++ (nullable NSString *)MIMETypeForExtension:(NSString *)extension {
+ if (extension == nil) {
+ return nil;
+ }
+
+ CFStringRef pathExtension = (__bridge_retained CFStringRef)extension;
+ CFStringRef type =
+ UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, pathExtension, NULL);
+ NSString *mimeType =
+ (__bridge_transfer NSString *)UTTypeCopyPreferredTagWithClass(type, kUTTagClassMIMEType);
+ CFRelease(pathExtension);
+ if (type != NULL) {
+ CFRelease(type);
+ }
+
+ return mimeType;
+}
+
++ (NSString *)queryStringForDictionary:(nullable NSDictionary *)dictionary {
+ if (!dictionary) {
+ return @"";
+ }
+
+ __block NSMutableArray *queryItems = [[NSMutableArray alloc] initWithCapacity:[dictionary count]];
+ [dictionary enumerateKeysAndObjectsUsingBlock:^(NSString *_Nonnull name, NSString *_Nonnull value,
+ BOOL *_Nonnull stop) {
+ NSString *item =
+ [FIRStorageUtils GCSEscapedString:[NSString stringWithFormat:@"%@=%@", name, value]];
+ [queryItems addObject:item];
+ }];
+ return [queryItems componentsJoinedByString:@"&"];
+}
+
++ (NSURLRequest *)defaultRequestForPath:(FIRStoragePath *)path {
+ NSMutableURLRequest *request = [[NSMutableURLRequest alloc] init];
+ NSURLComponents *components = [[NSURLComponents alloc] init];
+ [components setScheme:kFIRStorageScheme];
+ [components setHost:kFIRStorageHost];
+ NSString *encodedPath = [self encodedURLForPath:path];
+ [components setPercentEncodedPath:encodedPath];
+ [request setURL:components.URL];
+ return request;
+}
+
++ (NSString *)encodedURLForPath:(FIRStoragePath *)path {
+ NSString *bucketName = [FIRStorageUtils GCSEscapedString:path.bucket];
+ NSString *objectName = [FIRStorageUtils GCSEscapedString:path.object];
+ NSString *bucketFormat = [NSString stringWithFormat:kFIRStorageBucketPathFormat, bucketName];
+ NSString *urlPath = [@"/" stringByAppendingPathComponent:bucketFormat];
+ if (objectName) {
+ NSString *objectFormat = [NSString stringWithFormat:kFIRStorageObjectPathFormat, objectName];
+ urlPath = [urlPath stringByAppendingFormat:@"/%@", objectFormat];
+ } else {
+ urlPath = [urlPath stringByAppendingString:@"/o"];
+ }
+ return [@"/" stringByAppendingString:[kFIRStorageVersionPath stringByAppendingString:urlPath]];
+}
+
+@end
+
+@implementation NSDictionary (FIRStorageNSDictionaryJSONHelpers)
+
++ (nullable instancetype)frs_dictionaryFromJSONData:(nullable NSData *)data {
+ if (!data) {
+ return nil;
+ }
+ return [NSJSONSerialization JSONObjectWithData:data
+ options:NSJSONReadingMutableContainers
+ error:nil];
+}
+
+@end
+
+@implementation NSData (FIRStorageNSDataJSONHelpers)
+
++ (nullable instancetype)frs_dataFromJSONDictionary:(nullable NSDictionary *)dictionary {
+ if (!dictionary) {
+ return nil;
+ }
+ return [NSJSONSerialization dataWithJSONObject:dictionary options:0 error:nil];
+}
+
+@end \ No newline at end of file
diff --git a/Firebase/Storage/FirebaseStorage.h b/Firebase/Storage/FirebaseStorage.h
new file mode 100644
index 0000000..1e5dfa4
--- /dev/null
+++ b/Firebase/Storage/FirebaseStorage.h
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRStorage.h"
+#import "FIRStorageConstants.h"
+#import "FIRStorageDownloadTask.h"
+#import "FIRStorageMetadata.h"
+#import "FIRStorageObservableTask.h"
+#import "FIRStorageReference.h"
+#import "FIRStorageTask.h"
+#import "FIRStorageTaskSnapshot.h"
+#import "FIRStorageUploadTask.h"
diff --git a/Firebase/Storage/FirebaseStorage.podspec b/Firebase/Storage/FirebaseStorage.podspec
new file mode 100644
index 0000000..69c6ddc
--- /dev/null
+++ b/Firebase/Storage/FirebaseStorage.podspec
@@ -0,0 +1,44 @@
+# This podspec is not intended to be deployed. It is solely for the static
+# library framework build process at
+# https://github.com/firebase/firebase-ios-sdk/tree/master/BuildFrameworks
+
+Pod::Spec.new do |s|
+ s.name = 'FirebaseStorage'
+ s.version = '2.0.0'
+ s.summary = 'Firebase Open Source Libraries for iOS.'
+
+ s.description = <<-DESC
+Simplify your iOS development, grow your user base, and monetize more effectively with Firebase.
+ DESC
+
+ s.homepage = 'https://firebase.google.com'
+ s.license = { :type => 'Apache', :file => '../../LICENSE' }
+ s.authors = 'Google, Inc.'
+
+ # NOTE that the FirebaseDev pod is neither publicly deployed nor yet interchangeable with the
+ # Firebase pod
+ s.source = { :git => 'https://github.com/firebase/firebase-ios-sdk.git', :tag => s.version.to_s }
+ s.social_media_url = 'https://twitter.com/Firebase'
+ s.ios.deployment_target = '7.0'
+
+ s.source_files = '**/*.[mh]'
+ s.public_header_files =
+ 'FirebaseStorage.h',
+ 'FIRStorage.h',
+ 'FIRStorageConstants.h',
+ 'FIRStorageDownloadTask.h',
+ 'FIRStorageMetadata.h',
+ 'FIRStorageObservableTask.h',
+ 'FIRStorageReference.h',
+ 'FIRStorageSwiftNameSupport.h',
+ 'FIRStorageTask.h',
+ 'FIRStorageTaskSnapshot.h',
+ 'FIRStorageUploadTask.h'
+
+ s.framework = 'MobileCoreServices'
+# s.dependency 'FirebaseDev/Core'
+ s.dependency 'GTMSessionFetcher/Core', '~> 1.1'
+ s.xcconfig = { 'GCC_PREPROCESSOR_DEFINITIONS' =>
+ '$(inherited) ' +
+ 'FIRStorage_VERSION=' + s.version.to_s }
+end
diff --git a/Firebase/Storage/Private/FIRStorageConstants_Private.h b/Firebase/Storage/Private/FIRStorageConstants_Private.h
new file mode 100644
index 0000000..50addb1
--- /dev/null
+++ b/Firebase/Storage/Private/FIRStorageConstants_Private.h
@@ -0,0 +1,145 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+@class FIRStorageMetadata;
+
+NS_ASSUME_NONNULL_BEGIN
+
+FOUNDATION_EXPORT NSString *const kGCSScheme;
+FOUNDATION_EXPORT NSString *const kGCSHost;
+FOUNDATION_EXPORT NSString *const kGCSUploadPath;
+FOUNDATION_EXPORT NSString *const kGCSStorageVersionPath;
+FOUNDATION_EXPORT NSString *const kGCSBucketPathFormat;
+FOUNDATION_EXPORT NSString *const kGCSObjectPathFormat;
+
+FOUNDATION_EXPORT NSString *const kFIRStorageScheme;
+FOUNDATION_EXPORT NSString *const kFIRStorageHost;
+FOUNDATION_EXPORT NSString *const kFIRStorageVersionPath;
+FOUNDATION_EXPORT NSString *const kFIRStorageBucketPathFormat;
+FOUNDATION_EXPORT NSString *const kFIRStorageObjectPathFormat;
+FOUNDATION_EXPORT NSString *const kFIRStorageFullPathFormat;
+
+FOUNDATION_EXPORT NSString *const kFIRStorageAuthTokenFormat;
+FOUNDATION_EXPORT NSString *const kFIRStorageDefaultBucketFormat;
+
+FOUNDATION_EXPORT NSString *const kFIRStorageResponseErrorDomain;
+FOUNDATION_EXPORT NSString *const kFIRStorageResponseErrorCode;
+FOUNDATION_EXPORT NSString *const kFIRStorageResponseBody;
+
+FOUNDATION_EXPORT NSString *const kFIRStorageTaskStatusResumeNotification;
+FOUNDATION_EXPORT NSString *const kFIRStorageTaskStatusPauseNotification;
+FOUNDATION_EXPORT NSString *const kFIRStorageTaskStatusProgressNotification;
+FOUNDATION_EXPORT NSString *const kFIRStorageTaskStatusCompleteNotification;
+FOUNDATION_EXPORT NSString *const kFIRStorageTaskStatusFailureNotification;
+
+FOUNDATION_EXPORT NSString *const kFIRStorageMetadataBucket;
+FOUNDATION_EXPORT NSString *const kFIRStorageMetadataCacheControl;
+FOUNDATION_EXPORT NSString *const kFIRStorageMetadataContentDisposition;
+FOUNDATION_EXPORT NSString *const kFIRStorageMetadataContentEncoding;
+FOUNDATION_EXPORT NSString *const kFIRStorageMetadataContentLanguage;
+FOUNDATION_EXPORT NSString *const kFIRStorageMetadataContentType;
+FOUNDATION_EXPORT NSString *const kFIRStorageMetadataCustomMetadata;
+FOUNDATION_EXPORT NSString *const kFIRStorageMetadataSize;
+FOUNDATION_EXPORT NSString *const kFIRStorageMetadataDownloadURLs;
+FOUNDATION_EXPORT NSString *const kFIRStorageMetadataGeneration;
+FOUNDATION_EXPORT NSString *const kFIRStorageMetadataMetageneration;
+FOUNDATION_EXPORT NSString *const kFIRStorageMetadataTimeCreated;
+FOUNDATION_EXPORT NSString *const kFIRStorageMetadataUpdated;
+FOUNDATION_EXPORT NSString *const kFIRStorageMetadataName;
+FOUNDATION_EXPORT NSString *const kFIRStorageMetadataDownloadTokens;
+
+FOUNDATION_EXPORT NSString *const kFIRStorageInvalidDataFormat;
+FOUNDATION_EXPORT NSString *const kFIRStorageInvalidObserverStatus;
+
+FOUNDATION_EXPORT NSString *const kFIRStorageBundleIdentifier;
+
+/**
+ * Enum representing the internal state of an upload or download task.
+ */
+typedef NS_ENUM(NSInteger, FIRStorageTaskState) {
+ /**
+ * Unknown task state
+ */
+ FIRStorageTaskStateUnknown,
+
+ /**
+ * Task is being queued is ready to run
+ */
+ FIRStorageTaskStateQueueing,
+
+ /**
+ * Task is resuming from a paused state
+ */
+ FIRStorageTaskStateResuming,
+
+ /**
+ * Task is currently running
+ */
+ FIRStorageTaskStateRunning,
+
+ /**
+ * Task reporting a progress event
+ */
+ FIRStorageTaskStateProgress,
+
+ /**
+ * Task is pausing
+ */
+ FIRStorageTaskStatePausing,
+
+ /**
+ * Task is completing successfully
+ */
+ FIRStorageTaskStateCompleting,
+
+ /**
+ * Task is failing unrecoverably
+ */
+ FIRStorageTaskStateFailing,
+
+ /**
+ * Task paused successfully
+ */
+ FIRStorageTaskStatePaused,
+
+ /**
+ * Task cancelled successfully
+ */
+ FIRStorageTaskStateCancelled,
+
+ /**
+ * Task completed successfully
+ */
+ FIRStorageTaskStateSuccess,
+
+ /**
+ * Task failed unrecoverably
+ */
+ FIRStorageTaskStateFailed
+};
+
+/**
+ * Represents the various types of metadata: Files or Folders.
+ */
+typedef NS_ENUM(NSUInteger, FIRStorageMetadataType) {
+ FIRStorageMetadataTypeUnknown,
+ FIRStorageMetadataTypeFile,
+ FIRStorageMetadataTypeFolder,
+};
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Storage/Private/FIRStorageDeleteTask.h b/Firebase/Storage/Private/FIRStorageDeleteTask.h
new file mode 100644
index 0000000..c97fd27
--- /dev/null
+++ b/Firebase/Storage/Private/FIRStorageDeleteTask.h
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRStorageTask.h"
+
+@class GTMSessionFetcherService;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * Task which provides the ability to delete an object in Firebase Storage.
+ */
+@interface FIRStorageDeleteTask : FIRStorageTask<FIRStorageTaskManagement>
+
+- (instancetype)initWithReference:(FIRStorageReference *)reference
+ fetcherService:(GTMSessionFetcherService *)service
+ completion:(FIRStorageVoidError)completion;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Storage/Private/FIRStorageDownloadTask_Private.h b/Firebase/Storage/Private/FIRStorageDownloadTask_Private.h
new file mode 100644
index 0000000..293d1d5
--- /dev/null
+++ b/Firebase/Storage/Private/FIRStorageDownloadTask_Private.h
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@class GTMSessionFetcherService;
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FIRStorageDownloadTask ()
+
+/**
+ * Bytes which have been downloaded so far.
+ */
+@property(readonly, nonatomic) NSData *downloadData;
+
+/**
+ * The file on disk to write to.
+ */
+@property(copy, nonatomic) NSURL *fileURL;
+
+/**
+ * Initializes a download task with a base FIRStorageReference and GTMSessionFetcherService.
+ * @param reference The base FIRStorageReference which fetchers use for configuration.
+ * @param service The GTMSessionFetcherService which will create fetchers.
+ * @param fileURL The system URL to download to. If nil, download in memory as bytes.
+ * @return Returns an instance of FIRStorageDownloadTask
+ */
+- (instancetype)initWithReference:(FIRStorageReference *)reference
+ fetcherService:(GTMSessionFetcherService *)service
+ file:(nullable NSURL *)fileURL;
+
+/**
+ * Cancels the download task and passes an appropriate error to the developer.
+ * @param error NSError to propegate to the developer.
+ */
+- (void)cancelWithError:(NSError *)error;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Storage/Private/FIRStorageErrors.h b/Firebase/Storage/Private/FIRStorageErrors.h
new file mode 100644
index 0000000..7c236d9
--- /dev/null
+++ b/Firebase/Storage/Private/FIRStorageErrors.h
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRStorageConstants.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@class FIRStorageReference;
+
+/**
+ * Adds wrappers for common Firebase Storage errors (including creating errors from GCS errors).
+ * For more information on unwrapping GCS errors, see the GCS errors docs:
+ * https://cloud.google.com/storage/docs/json_api/v1/status-codes
+ * This is never publicly exposed to end developers (as they will simply see an NSError).
+ */
+@interface FIRStorageErrors : NSObject
+
+/**
+ * Creates a Firebase Storage error from a specific FIRStorageErrorCode.
+ */
++ (NSError *)errorWithCode:(FIRStorageErrorCode)code;
+
+/**
+ * Creates a Firebase Storage error from a specific FIRStorageErrorCode while adding
+ * custom info from an optionally provided info dictionary.
+ */
++ (NSError *)errorWithCode:(FIRStorageErrorCode)code
+ infoDictionary:(nullable NSDictionary *)dictionary;
+
+/**
+ * Creates a Firebase Storage error from a specific GCS error and FIRStorageReference.
+ * @param error Server error to wrap and return as a Firebase Storage error.
+ * @param reference FIRStorageReference which provides context about the request being made.
+ * @return Returns an Firebase Storage error, or nil if no error is provided.
+ */
++ (nullable NSError *)errorWithServerError:(nullable NSError *)error
+ reference:(nullable FIRStorageReference *)reference;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Storage/Private/FIRStorageGetMetadataTask.h b/Firebase/Storage/Private/FIRStorageGetMetadataTask.h
new file mode 100644
index 0000000..5f1dc8f
--- /dev/null
+++ b/Firebase/Storage/Private/FIRStorageGetMetadataTask.h
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRStorageTask.h"
+
+@class GTMSessionFetcherService;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * Task which provides the ability to get metadata on an object in Firebase Storage.
+ */
+@interface FIRStorageGetMetadataTask : FIRStorageTask<FIRStorageTaskManagement>
+
+- (instancetype)initWithReference:(FIRStorageReference *)reference
+ fetcherService:(GTMSessionFetcherService *)service
+ completion:(FIRStorageVoidMetadataError)completion;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Storage/Private/FIRStorageMetadata_Private.h b/Firebase/Storage/Private/FIRStorageMetadata_Private.h
new file mode 100644
index 0000000..629c935
--- /dev/null
+++ b/Firebase/Storage/Private/FIRStorageMetadata_Private.h
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRStorageConstants_Private.h"
+
+@class FIRStorageReference;
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FIRStorageMetadata ()
+
+@property(readwrite, nonatomic) NSString *name;
+
+@property(readwrite, nonatomic) NSString *path;
+
+@property(readwrite, nonatomic) FIRStorageReference *reference;
+
+/**
+ * The type of the object, either a "File" or a "Folder".
+ */
+@property(readwrite) FIRStorageMetadataType type;
+
+/**
+ * Returns an RFC3339 formatted date from a string.
+ * @param dateString An NSString of the form: yyyy-MM-ddTHH:mm:ss.SSSZ.
+ * @return An NSDate populated from the string or nil if conversion isn't possible.
+ */
+- (nullable NSDate *)dateFromRFC3339String:(NSString *)dateString;
+
+/**
+ * Returns an RFC3339 formatted string from an NSDate object.
+ * @param date The NSDate object to be converted to a string.
+ * @return An NSString of the form: yyyy-MM-ddTHH:mm:ss.SSSZ or nil if conversion isn't possible.
+ */
+- (nullable NSString *)RFC3339StringFromDate:(NSDate *)date;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Storage/Private/FIRStorageObservableTask_Private.h b/Firebase/Storage/Private/FIRStorageObservableTask_Private.h
new file mode 100644
index 0000000..e37b63f
--- /dev/null
+++ b/Firebase/Storage/Private/FIRStorageObservableTask_Private.h
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+NS_ASSUME_NONNULL_BEGIN
+
+@class FIRStorageTaskSnapshot;
+
+@class GTMSessionFetcherService;
+
+@interface FIRStorageObservableTask ()
+
+/**
+ * Creates a new FIRStorageTask initialized with a FIRStorageReference and GTMSessionFetcherService.
+ * @param reference A FIRStorageReference the task will be performed on.
+ * @param service A GTMSessionFetcherService which provides the fetchers and configuration for
+ * requests.
+ * @return A new FIRStorageTask representing the current task.
+ */
+- (instancetype)initWithReference:(FIRStorageReference *)reference
+ fetcherService:(GTMSessionFetcherService *)service;
+
+/**
+ * Raise events for a given task status by passing along a snapshot of existing task state.
+ * @param status A FIRStorageTaskStatus to raise events for.
+ * @param snapshot A FIRStorageTaskSnapshot snapshot of task state to pass through the handler.
+ */
+- (void)fireHandlersForStatus:(FIRStorageTaskStatus)status
+ snapshot:(FIRStorageTaskSnapshot *)snapshot;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Storage/Private/FIRStoragePath.h b/Firebase/Storage/Private/FIRStoragePath.h
new file mode 100644
index 0000000..53ff7ef
--- /dev/null
+++ b/Firebase/Storage/Private/FIRStoragePath.h
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * Represents a path in GCS, which can be represented as: gs://bucket/path/to/object
+ * or http[s]://firebasestorage.googleapis.com/v0/b/bucket/o/path/to/object?token=<12345>
+ * This class also includes helper methods to parse those URI/Ls, as well as to
+ * add and remove path segments.
+ */
+@interface FIRStoragePath : NSObject
+
+/**
+ * The GCS bucket in the path.
+ */
+@property(copy, nonatomic) NSString *bucket;
+
+/**
+ * The GCS object in the path.
+ */
+@property(copy, nonatomic, nullable) NSString *object;
+
+/**
+ * Parses a generic string (representing some URI or URL) and returns the appropriate path.
+ * @param string String which is parsed into a path.
+ * @return Returns an instance of FIRStoragePath or nil if one can't be created.
+ * @throws Throws an exception if the string is not a valid gs:// URI or http[s]:// URL.
+ */
++ (nullable FIRStoragePath *)pathFromString:(NSString *)string;
+
+/**
+ * Parses a gs://bucket/path/to/object URI into a GCS path.
+ * @param aURIString gs:// URI which is parsed into a path.
+ * @return Returns an instance of FIRStoragePath or nil if one can't be created.
+ * @throws Throws an exception if the string is not a valid gs:// URI.
+ */
++ (nullable FIRStoragePath *)pathFromGSURI:(NSString *)aURIString;
+
+- (instancetype)init NS_UNAVAILABLE;
+
+/**
+ * Constructs an FIRStoragePath object that represents the given bucket and object.
+ * @param bucket The name of the bucket.
+ * @param object The name of the object.
+ * @return An instance of FIRStoragePath representing the @a bucket and @a object.
+ */
+- (instancetype)initWithBucket:(NSString *)bucket
+ object:(nullable NSString *)object NS_DESIGNATED_INITIALIZER;
+
+/**
+ * Parses a http[s]://firebasestorage.googleapis.com/v0/b/bucket/o/path/to/object...?token=<12345>
+ * URL into a GCS path.
+ * @param aURLString http[s]:// URL which is parsed into a path.
+ * string which is parsed into a path.
+ * @return Returns an instance of FIRStoragePath or nil if one can't be created.
+ * @throws Throws an exception if the string is not a valid http[s]:// URL.
+ */
++ (nullable FIRStoragePath *)pathFromHTTPURL:(NSString *)aURLString;
+
+/**
+ * Creates a new path based off of the current path and a string appended to it.
+ * Note that all slashes are compressed to a single slash, and leading and trailing slashes
+ * are removed.
+ * @param path String to append to the current path.
+ * @return Returns a new instance of FIRStoragePath with the new path appended.
+ */
+- (FIRStoragePath *)child:(NSString *)path;
+
+/**
+ * Creates a new path based off of the current path with the last path segment removed.
+ * @return Returns a new instance of FIRStoragePath pointing to the parent path,
+ * or nil if the current path points to the root.
+ */
+- (nullable FIRStoragePath *)parent;
+
+/**
+ * Creates a new path based off of the root of the bucket.
+ * @return Returns a new instance of FIRStoragePath pointing to the root of the bucket.
+ */
+- (FIRStoragePath *)root;
+
+/**
+ * Returns a GS URI representing the current path.
+ * @return Returns a gs://bucket/path/to/object URI representing the current path.
+ */
+- (NSString *)stringValue;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Storage/Private/FIRStorageReference_Private.h b/Firebase/Storage/Private/FIRStorageReference_Private.h
new file mode 100644
index 0000000..825964d
--- /dev/null
+++ b/Firebase/Storage/Private/FIRStorageReference_Private.h
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRStoragePath.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FIRStorageReference ()
+
+@property(nonatomic, readwrite) FIRStorage *storage;
+
+/**
+ * The current path which points to an object in the Google Cloud Storage bucket.
+ */
+@property(strong, nonatomic) FIRStoragePath *path;
+
+- (instancetype)initWithStorage:(FIRStorage *)storage
+ path:(FIRStoragePath *)path NS_DESIGNATED_INITIALIZER;
+
+- (NSString *)stringValue;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Storage/Private/FIRStorageTaskSnapshot_Private.h b/Firebase/Storage/Private/FIRStorageTaskSnapshot_Private.h
new file mode 100644
index 0000000..1762a61
--- /dev/null
+++ b/Firebase/Storage/Private/FIRStorageTaskSnapshot_Private.h
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRStorageConstants_Private.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@class FIRStorageMetadata;
+@class FIRStorageReference;
+@class FIRStorageTask;
+
+@interface FIRStorageTaskSnapshot ()
+
+@property(readwrite, copy, nonatomic) FIRStorageTask *task;
+@property(readwrite, copy, nonatomic) FIRStorageMetadata *metadata;
+@property(readwrite, copy, nonatomic) FIRStorageReference *reference;
+@property(readwrite, strong, nonatomic) NSProgress *progress;
+@property(readwrite, copy, nonatomic) NSError *error;
+
+/**
+ * Creates a new task snapshot from the given properties.
+ * @param task The task being represented in this snapshot.
+ * @param state The current state of the parent task.
+ * @param metadata The FIRStorageMetadata of a task. Before upload/update, contains the metadata
+ * to be updated; after, contains the returned metadata. May be nil if no metadata is provided
+ * or returned.
+ * @param reference The FIRStorageReference that spawned the task this snapshot is based on.
+ * @param progress An NSProgress object containing progress of the task this snapshot is based on,
+ * or nil if the task doesn't report progress.
+ * @param error An NSError object containing an error that occurred during the task,
+ * if one occurred.
+ * @return Returns the constructed snapshot.
+ */
+- (instancetype)initWithTask:(__kindof FIRStorageTask *)task
+ state:(FIRStorageTaskState)state
+ metadata:(nullable FIRStorageMetadata *)metadata
+ reference:(FIRStorageReference *)reference
+ progress:(nullable NSProgress *)progress
+ error:(nullable NSError *)error;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Storage/Private/FIRStorageTask_Private.h b/Firebase/Storage/Private/FIRStorageTask_Private.h
new file mode 100644
index 0000000..598006b
--- /dev/null
+++ b/Firebase/Storage/Private/FIRStorageTask_Private.h
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRStorageConstants_Private.h"
+#import "FIRStorageErrors.h"
+#import "FIRStorageReference.h"
+#import "FIRStorageReference_Private.h"
+#import "FIRStorageTaskSnapshot.h"
+#import "FIRStorageTaskSnapshot_Private.h"
+#import "FIRStorageUtils.h"
+
+#import <GTMSessionFetcher/GTMSessionFetcher.h>
+#import <GTMSessionFetcher/GTMSessionFetcherService.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FIRStorageTask ()
+
+/**
+ * State for the current task in progress.
+ */
+@property(atomic) FIRStorageTaskState state;
+
+/**
+ * FIRStorageMetadata for the task in progress, or nil if none present.
+ */
+@property(strong, nonatomic, nullable) FIRStorageMetadata *metadata;
+
+/**
+ * Error which occurred during task execution, or nil if no error occurred.
+ */
+@property(strong, nonatomic, nullable) NSError *error;
+
+/**
+ * NSProgress object which tracks the progess of an observable task.
+ */
+@property(strong, nonatomic) NSProgress *progress;
+
+/**
+ * Reference pointing to the location the task is being performed against.
+ */
+@property(strong, nonatomic) FIRStorageReference *reference;
+
+@property(strong, readwrite, nonatomic, nonnull) FIRStorageTaskSnapshot *snapshot;
+
+@property(readonly, copy, nonatomic) NSURLRequest *baseRequest;
+
+@property(strong, atomic) GTMSessionFetcher *fetcher;
+
+@property(readonly, nonatomic) GTMSessionFetcherService *fetcherService;
+
+/**
+ * Creates a new FIRStorageTask initialized with a FIRStorageReference and GTMSessionFetcherService.
+ * @param reference A FIRStorageReference the task will be performed on.
+ * @param service A GTMSessionFetcherService which provides the fetchers and configuration for
+ * requests.
+ * @return A new FIRStorageTask representing the current task.
+ */
+- (instancetype)initWithReference:(FIRStorageReference *)reference
+ fetcherService:(GTMSessionFetcherService *)service NS_DESIGNATED_INITIALIZER;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Storage/Private/FIRStorageTokenAuthorizer.h b/Firebase/Storage/Private/FIRStorageTokenAuthorizer.h
new file mode 100644
index 0000000..78a8218
--- /dev/null
+++ b/Firebase/Storage/Private/FIRStorageTokenAuthorizer.h
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <GTMSessionFetcher/GTMSessionFetcherService.h>
+
+@class FIRApp;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * Wrapper class for FIRApp that implements the GTMFetcherAuthorizationProtocol,
+ * so as to easily provide GTMSessionFetcher fetches a Firebase Authentication JWT
+ * for the current logged in user. Handles token expiration and other failure cases.
+ * If no authentication provider exists or no token is found, no token is added
+ * and the request is passed.
+ */
+@interface FIRStorageTokenAuthorizer : NSObject<GTMFetcherAuthorizationProtocol>
+
+/**
+ * Initializes the token authorizer with an instance of FIRApp.
+ * @param app An instance of FIRApp which provides auth tokens.
+ * @return Returns an instance of FIRStorageTokenAuthorizer which adds the appropriate
+ * "Authorization" header to all outbound requests. Note that a token may not be added
+ * if a getTokenImplementation doesn't exist on FIRApp. This allows for unauthenticated
+ * access, if Firebase Storage rules allow for it.
+ */
+- (instancetype)initWithApp:(FIRApp *)app fetcherService:(GTMSessionFetcherService *)service;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Storage/Private/FIRStorageUpdateMetadataTask.h b/Firebase/Storage/Private/FIRStorageUpdateMetadataTask.h
new file mode 100644
index 0000000..2fcefdd
--- /dev/null
+++ b/Firebase/Storage/Private/FIRStorageUpdateMetadataTask.h
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRStorageTask.h"
+
+@class GTMSessionFetcherService;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * Task which provides the ability update the metadata on an object in Firebase Storage.
+ */
+@interface FIRStorageUpdateMetadataTask : FIRStorageTask<FIRStorageTaskManagement>
+
+- (instancetype)initWithReference:(FIRStorageReference *)reference
+ fetcherService:(GTMSessionFetcherService *)service
+ metadata:(FIRStorageMetadata *)metadata
+ completion:(FIRStorageVoidMetadataError)completion;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Storage/Private/FIRStorageUploadTask_Private.h b/Firebase/Storage/Private/FIRStorageUploadTask_Private.h
new file mode 100644
index 0000000..468d9d3
--- /dev/null
+++ b/Firebase/Storage/Private/FIRStorageUploadTask_Private.h
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@class GTMSessionUploadFetcher;
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FIRStorageUploadTask ()
+
+/**
+ * The data to be uploaded (if uploading bytes).
+ */
+@property(readonly, copy, nonatomic, nullable) NSData *uploadData;
+
+/**
+ * The name of a file on disk to be uploaded (if uploading from a file).
+ */
+@property(readonly, copy, nonatomic, nullable) NSURL *fileURL;
+
+/**
+ * The FIRStorageMetadata about the object being uploaded.
+ */
+@property(readonly, copy, nonatomic) FIRStorageMetadata *uploadMetadata;
+
+/**
+ * GTMSessionUploadFetcher used by all uploads.
+ */
+@property(strong, atomic) GTMSessionUploadFetcher *uploadFetcher;
+
+/**
+ * Initializes an upload task with a base FIRStorageReference and GTMSessionFetcherService.
+ * @param reference The base FIRStorageReference which fetchers use for configuration.
+ * @param service The GTMSessionFetcherService which will create fetchers.
+ * @param uploadData The NSData object to be uploaded.
+ * @return Returns an instance of FIRStorageUploadTask.
+ */
+- (instancetype)initWithReference:(FIRStorageReference *)reference
+ fetcherService:(GTMSessionFetcherService *)service
+ data:(NSData *)uploadData
+ metadata:(FIRStorageMetadata *)metadata;
+
+/**
+ * Initializes an upload task with a base FIRStorageReference and GTMSessionFetcherService.
+ * @param reference The base FIRStorageReference which fetchers use for configuration.
+ * @param service The GTMSessionFetcherService which will create fetchers.
+ * @param fileURL The system file URL to upload from.
+ * @return Returns an instance of FIRStorageUploadTask.
+ */
+- (instancetype)initWithReference:(FIRStorageReference *)reference
+ fetcherService:(GTMSessionFetcherService *)service
+ file:(NSURL *)fileURL
+ metadata:(FIRStorageMetadata *)metadata;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Storage/Private/FIRStorageUtils.h b/Firebase/Storage/Private/FIRStorageUtils.h
new file mode 100644
index 0000000..e687c82
--- /dev/null
+++ b/Firebase/Storage/Private/FIRStorageUtils.h
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+@class FIRStoragePath;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * FIRStorageUtils provides a number of helper methods for commonly used operations
+ * in Firebase Storage, such as JSON parsing, escaping, and file extensions.
+ */
+@interface FIRStorageUtils : NSObject
+
+/**
+ * Returns a percent encoded string appropriate for GCS.
+ * See https://cloud.google.com/storage/docs/naming for more details.
+ * @param string A path to escape characters according to the GCS
+ * @return A percent encoded string appropriate for GCS operations or nil if string is nil
+ * or can't be escaped.
+ */
++ (nullable NSString *)GCSEscapedString:(NSString *)string;
+
+/**
+ * Returns the MIME type for a file extension.
+ * Example of how to get MIME type here: http://ddeville.me/2011/12/mime-to-UTI-cocoa/
+ * @param extension A file extension such as "txt", "png", etc.
+ * @return The MIME type for the input extension such as "text/plain", "image/png", etc.
+ * or nil if no type is found.
+ */
++ (nullable NSString *)MIMETypeForExtension:(NSString *)extension;
+
+/**
+ * Returns a properly escaped query string from a given dictionary of query items to values.
+ * @param dictionary A dictionary containing query items and associated values.
+ * @return A properly escaped query string or the empty string for a nil or empty dictionary.
+ */
++ (NSString *)queryStringForDictionary:(nullable NSDictionary *)dictionary;
+
+/**
+ * Returns a base NSURLRequest used by all tasks.
+ * @param path The FIRStoragePath to create a request for.
+ * @return Returns a properly formatted NSURLRequest of the form:
+ * scheme://host/version/b/<bucket>/o[/path/to/object]
+ */
++ (NSURLRequest *)defaultRequestForPath:(FIRStoragePath *)path;
+
+/**
+ * Creates the appropriate GCS percent escaped path for a given FIRStoragePath.
+ * @param path The FIRStoragePath to encode.
+ * @return Returns the GCS encoded URL for a given FIRStoragePath.
+ */
++ (NSString *)encodedURLForPath:(FIRStoragePath *)path;
+
+@end
+
+@interface NSDictionary (FIRStorageNSDictionaryJSONHelpers)
+
+/**
+ * Returns a dictionary representation of the data in @a data.
+ * @param data NSData containing JSON data.
+ * @return An NSDictionary representation of the JSON, or nil if serialization failed.
+ */
++ (nullable instancetype)frs_dictionaryFromJSONData:(nullable NSData *)data;
+
+@end
+
+@interface NSData (FIRStorageNSDataJSONHelpers)
+
+/**
+ * Returns an NSData instance containing JSON serialized from @a dictionary.
+ * @param dictionary An NSDictionary containing only types serializable to JSON.
+ * @return An NSData object representing the binary JSON, or nil if serialization failed.
+ */
++ (nullable instancetype)frs_dataFromJSONDictionary:(nullable NSDictionary *)dictionary;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Storage/Private/FIRStorage_Private.h b/Firebase/Storage/Private/FIRStorage_Private.h
new file mode 100644
index 0000000..aefe808
--- /dev/null
+++ b/Firebase/Storage/Private/FIRStorage_Private.h
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@class FIRApp;
+@class GTMSessionFetcherService;
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FIRStorage ()
+
+@property(strong, nonatomic, readwrite) FIRApp *app;
+
+@property(strong, nonatomic) GTMSessionFetcherService *fetcherServiceForApp;
+
+@property(strong, nonatomic) NSString *storageBucket;
+
+/**
+ * Enables/disables GTMSessionFetcher HTTP logging
+ * @param isLoggingEnabled Boolean passed through to enable/disable GTMSessionFetcher logging
+ */
++ (void)setGTMSessionFetcherLoggingEnabled:(BOOL)isLoggingEnabled;
+
+@end
+
+NS_ASSUME_NONNULL_END