2 * Copyright (c) 2017 Apple Inc. All Rights Reserved.
4 * @APPLE_LICENSE_HEADER_START@
6 * This file contains Original Code and/or Modifications of Original Code
7 * as defined in and that are subject to the Apple Public Source License
8 * Version 2.0 (the 'License'). You may not use this file except in
9 * compliance with the License. Please obtain a copy of the License at
10 * http://www.opensource.apple.com/apsl/ and read it before using this
13 * The Original Code and all software distributed under the License are
14 * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
15 * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
16 * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
17 * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
18 * Please see the License for the specific language governing rights and
19 * limitations under the License.
21 * @APPLE_LICENSE_HEADER_END@
24 #import <Foundation/Foundation.h>
28 #import <CloudKit/CloudKit.h>
29 #import <CloudKit/CloudKit_Private.h>
31 #import "keychain/categories/NSError+UsefulConstructors.h"
32 #import "keychain/ckks/CloudKitDependencies.h"
33 #import "keychain/ckks/CloudKitCategories.h"
34 #import "keychain/ckks/CKKS.h"
35 #import "keychain/ckks/CKKSKeychainView.h"
36 #import "keychain/ckks/CKKSZoneStateEntry.h"
37 #import "keychain/ckks/CKKSFetchAllRecordZoneChangesOperation.h"
38 #import "keychain/ckks/CKKSMirrorEntry.h"
39 #import "keychain/ckks/CKKSManifest.h"
40 #import "keychain/ckks/CKKSManifestLeafRecord.h"
41 #import "NSError+UsefulConstructors.h"
42 #import "CKKSPowerCollection.h"
43 #include <securityd/SecItemServer.h>
45 @implementation CKKSCloudKitFetchRequest
48 @implementation CKKSCloudKitDeletion
49 - (instancetype)initWithRecordID:(CKRecordID*)recordID recordType:(NSString*)recordType
51 if((self = [super init])) {
53 _recordType = recordType;
60 @interface CKKSFetchAllRecordZoneChangesOperation()
61 @property CKDatabaseOperation<CKKSFetchRecordZoneChangesOperation>* fetchRecordZoneChangesOperation;
62 @property NSMutableDictionary<CKRecordZoneID*, CKFetchRecordZoneChangesConfiguration*>* allClientOptions;
64 @property CKOperationGroup* ckoperationGroup;
65 @property (assign) NSUInteger fetchedItems;
66 @property bool forceResync;
69 @implementation CKKSFetchAllRecordZoneChangesOperation
71 // Sets up an operation to fetch all changes from the server, and collect them until we know if the fetch completes.
72 // As a bonus, you can depend on this operation without worry about NSOperation completion block dependency issues.
74 - (instancetype)init {
78 - (instancetype)initWithContainer:(CKContainer*)container
79 fetchClass:(Class<CKKSFetchRecordZoneChangesOperation>)fetchRecordZoneChangesOperationClass
80 clients:(NSArray<id<CKKSChangeFetcherClient>>*)clients
81 fetchReasons:(NSSet<CKKSFetchBecause*>*)fetchReasons
82 apnsPushes:(NSSet<CKRecordZoneNotification*>*)apnsPushes
83 forceResync:(bool)forceResync
84 ckoperationGroup:(CKOperationGroup*)ckoperationGroup
86 if(self = [super init]) {
87 _container = container;
88 _fetchRecordZoneChangesOperationClass = fetchRecordZoneChangesOperationClass;
90 NSMutableDictionary* clientMap = [NSMutableDictionary dictionary];
91 for(id<CKKSChangeFetcherClient> client in clients) {
92 clientMap[client.zoneID] = client;
94 _clientMap = [clientMap copy];
96 _ckoperationGroup = ckoperationGroup;
97 _forceResync = forceResync;
98 _fetchReasons = fetchReasons;
99 _apnsPushes = apnsPushes;
101 _modifications = [[NSMutableDictionary alloc] init];
102 _deletions = [[NSMutableDictionary alloc] init];
108 __weak __typeof(self) weakSelf = self;
110 // Ask all our clients for their change tags
111 self.allClientOptions = [NSMutableDictionary dictionary];
112 self.fetchedZoneIDs = [NSMutableArray array];
114 // Unused until [<rdar://problem/38725728> Changes to discretionary-ness (explicit or derived from QoS) should be "live"] has happened and we can determine network
115 // discretionaryness.
116 //bool nilChangeTag = false;
118 for(CKRecordZoneID* clientZoneID in self.clientMap) {
119 id<CKKSChangeFetcherClient> client = self.clientMap[clientZoneID];
121 CKKSCloudKitFetchRequest* clientPreference = [client participateInFetch];
122 if(clientPreference.participateInFetch) {
123 [self.fetchedZoneIDs addObject:client.zoneID];
125 CKFetchRecordZoneChangesConfiguration* options = [[CKFetchRecordZoneChangesConfiguration alloc] init];
127 if(!self.forceResync) {
128 options.previousServerChangeToken = clientPreference.changeToken;
131 //if(options.previousServerChangeToken == nil) {
132 // nilChangeTag = true;
135 self.allClientOptions[client.zoneID] = options;
139 if(self.fetchedZoneIDs.count == 0) {
140 // No clients actually want to fetch right now, so quit
141 self.error = [NSError errorWithDomain:CKKSErrorDomain code:CKKSNoFetchesRequested description:@"No clients want a fetch right now"];
142 secnotice("ckksfetch", "Cancelling fetch: %@", self.error);
146 // Compute the network discretionary approach this fetch will take.
147 // For now, everything is nondiscretionary, because we can't afford to block a nondiscretionary request later.
148 // Once [<rdar://problem/38725728> Changes to discretionary-ness (explicit or derived from QoS) should be "live"] happens, we can come back through and make things
149 // discretionary, but boost them later.
152 // If there's a nil change tag, go to nondiscretionary. This is likely a zone bringup (which happens during iCloud sign-in) or a resync (which happens due to user input)
153 // If the fetch reasons include an API fetch, an initial start or a key hierarchy fetch, become nondiscretionary as well.
155 CKOperationDiscretionaryNetworkBehavior networkBehavior = CKOperationDiscretionaryNetworkBehaviorNonDiscretionary;
157 // [self.fetchReasons containsObject:CKKSFetchBecauseAPIFetchRequest] ||
158 // [self.fetchReasons containsObject:CKKSFetchBecauseInitialStart] ||
159 // [self.fetchReasons containsObject:CKKSFetchBecauseKeyHierarchy]) {
160 // networkBehavior = CKOperationDiscretionaryNetworkBehaviorNonDiscretionary;
163 secnotice("ckks", "Beginning fetch with discretionary network (%d): %@", (int)networkBehavior, self.allClientOptions);
164 self.fetchRecordZoneChangesOperation = [[self.fetchRecordZoneChangesOperationClass alloc] initWithRecordZoneIDs:self.fetchedZoneIDs
165 configurationsByRecordZoneID:self.allClientOptions];
167 self.fetchRecordZoneChangesOperation.fetchAllChanges = YES;
168 self.fetchRecordZoneChangesOperation.configuration.discretionaryNetworkBehavior = networkBehavior;
169 self.fetchRecordZoneChangesOperation.group = self.ckoperationGroup;
170 secnotice("ckksfetch", "Operation group is %@", self.ckoperationGroup);
172 self.fetchRecordZoneChangesOperation.recordChangedBlock = ^(CKRecord *record) {
173 __strong __typeof(weakSelf) strongSelf = weakSelf;
174 secinfo("ckksfetch", "CloudKit notification: record changed(%@): %@", [record recordType], record);
176 // Add this to the modifications, and remove it from the deletions
177 strongSelf.modifications[record.recordID] = record;
178 [strongSelf.deletions removeObjectForKey:record.recordID];
179 strongSelf.fetchedItems++;
182 self.fetchRecordZoneChangesOperation.recordWithIDWasDeletedBlock = ^(CKRecordID *recordID, NSString *recordType) {
183 __strong __typeof(weakSelf) strongSelf = weakSelf;
184 secinfo("ckksfetch", "CloudKit notification: deleted record(%@): %@", recordType, recordID);
186 // Add to the deletions, and remove any pending modifications
187 [strongSelf.modifications removeObjectForKey: recordID];
188 strongSelf.deletions[recordID] = [[CKKSCloudKitDeletion alloc] initWithRecordID:recordID recordType:recordType];
189 strongSelf.fetchedItems++;
192 self.fetchRecordZoneChangesOperation.recordZoneChangeTokensUpdatedBlock = ^(CKRecordZoneID *recordZoneID, CKServerChangeToken *serverChangeToken, NSData *clientChangeTokenData) {
193 __strong __typeof(weakSelf) strongSelf = weakSelf;
195 secinfo("ckksfetch", "Received a new server change token for %@: %@ %@", recordZoneID, serverChangeToken, clientChangeTokenData);
196 strongSelf.changeTokens[recordZoneID] = serverChangeToken;
199 self.fetchRecordZoneChangesOperation.recordZoneFetchCompletionBlock = ^(CKRecordZoneID *recordZoneID, CKServerChangeToken *serverChangeToken, NSData *clientChangeTokenData, BOOL moreComing, NSError * recordZoneError) {
200 __strong __typeof(weakSelf) strongSelf = weakSelf;
202 secerror("ckksfetch: received callback for released object");
206 id<CKKSChangeFetcherClient> client = strongSelf.clientMap[recordZoneID];
208 secerror("ckksfetch: no client registered for %@, so why did we get any data?", recordZoneID);
212 // First, filter the modifications and deletions for this zone
213 NSMutableArray<CKRecord*>* zoneModifications = [NSMutableArray array];
214 NSMutableArray<CKKSCloudKitDeletion*>* zoneDeletions = [NSMutableArray array];
216 [strongSelf.modifications enumerateKeysAndObjectsUsingBlock:^(CKRecordID* _Nonnull recordID,
217 CKRecord* _Nonnull record,
219 if([recordID.zoneID isEqual:recordZoneID]) {
220 ckksinfo("ckksfetch", recordZoneID, "Sorting record modification %@: %@", recordID, record);
221 [zoneModifications addObject:record];
225 [strongSelf.deletions enumerateKeysAndObjectsUsingBlock:^(CKRecordID* _Nonnull recordID,
226 CKKSCloudKitDeletion* _Nonnull deletion,
227 BOOL* _Nonnull stop) {
228 if([recordID.zoneID isEqual:recordZoneID]) {
229 ckksinfo("ckksfetch", recordZoneID, "Sorting record deletion %@: %@", recordID, deletion);
230 [zoneDeletions addObject:deletion];
234 ckksnotice("ckksfetch", recordZoneID, "Record zone fetch complete: changeToken=%@ clientChangeTokenData=%@ changed=%lu deleted=%lu error=%@", serverChangeToken, clientChangeTokenData,
235 (unsigned long)zoneModifications.count,
236 (unsigned long)zoneDeletions.count,
239 if(recordZoneError == nil) {
240 // Tell the client about these changes!
241 [client changesFetched:zoneModifications
242 deletedRecordIDs:zoneDeletions
243 oldChangeToken:strongSelf.allClientOptions[recordZoneID].previousServerChangeToken
244 newChangeToken:serverChangeToken];
245 ckksnotice("ckksfetch", recordZoneID, "Finished processing fetch");
249 // Completion blocks don't count for dependencies. Use this intermediate operation hack instead.
250 CKKSResultOperation* recordZoneChangesCompletedOperation = [CKKSResultOperation named:@"record-zone-changes-completed" withBlock:^{}];
252 // Called with overall operation success. As I understand it, this block will be called for every operation.
253 // In the case of, e.g., network failure, the recordZoneFetchCompletionBlock will not be called, but this one will.
254 self.fetchRecordZoneChangesOperation.fetchRecordZoneChangesCompletionBlock = ^(NSError * _Nullable operationError) {
255 __strong __typeof(weakSelf) strongSelf = weakSelf;
257 secerror("ckksfetch: received callback for released object");
261 secnotice("ckksfetch", "Record zone changes fetch complete: error=%@", operationError);
263 strongSelf.error = operationError;
266 [CKKSPowerCollection CKKSPowerEvent:kCKKSPowerEventFetchAllChanges
267 count:strongSelf.fetchedItems];
269 // Count record changes per zone
270 NSMutableDictionary<CKRecordZoneID*,NSNumber*>* recordChangesPerZone = [NSMutableDictionary dictionary];
271 NSNumber* totalModifications = [NSNumber numberWithUnsignedLong:strongSelf.modifications.count];
272 NSNumber* totalDeletions = [NSNumber numberWithUnsignedLong:strongSelf.deletions.count];
274 for(CKRecordID* recordID in strongSelf.modifications) {
275 NSNumber* last = recordChangesPerZone[recordID.zoneID];
276 recordChangesPerZone[recordID.zoneID] = [NSNumber numberWithUnsignedLong:1+(last ? [last unsignedLongValue] : 0)];
278 for(CKRecordID* recordID in strongSelf.deletions) {
279 NSNumber* last = recordChangesPerZone[recordID.zoneID];
280 recordChangesPerZone[recordID.zoneID] = [NSNumber numberWithUnsignedLong:1+(last ? [last unsignedLongValue] : 0)];
283 for(CKRecordZoneNotification* rz in strongSelf.apnsPushes) {
284 if(rz.ckksPushTracingEnabled) {
285 secnotice("ckksfetch", "Submitting post-fetch CKEventMetric due to notification %@", rz);
287 // Schedule submitting this metric on another operation, so hopefully CK will have marked this fetch as done by the time that fires?
288 CKEventMetric *metric = [[CKEventMetric alloc] initWithEventName:@"APNSPushMetrics"];
289 metric.isPushTriggerFired = true;
290 metric[@"push_token_uuid"] = rz.ckksPushTracingUUID;
291 metric[@"push_received_date"] = rz.ckksPushReceivedDate;
292 metric[@"push_event_name"] = @"CKKS Push";
294 metric[@"fetch_error"] = operationError ? @1 : @0;
295 metric[@"fetch_error_domain"] = operationError.domain;
296 metric[@"fetch_error_code"] = [NSNumber numberWithLong:operationError.code];
298 metric[@"total_modifications"] = totalModifications;
299 metric[@"total_deletions"] = totalDeletions;
300 for(CKRecordZoneID* zoneID in recordChangesPerZone) {
301 metric[zoneID.zoneName] = recordChangesPerZone[zoneID];
304 // Okay, we now have this metric. But, it's unclear if calling associateWithCompletedOperation in this block will work. So, do something silly with operation scheduling.
305 // Grab pointers to these things
306 CKContainer* container = strongSelf.container;
307 CKDatabaseOperation<CKKSFetchRecordZoneChangesOperation>* rzcOperation = strongSelf.fetchRecordZoneChangesOperation;
309 CKKSResultOperation* launchMetricOp = [CKKSResultOperation named:@"submit-metric" withBlock:^{
310 if(![metric associateWithCompletedOperation:rzcOperation]) {
311 secerror("ckksfetch: Couldn't associate metric with operation: %@ %@", metric, rzcOperation);
313 [container submitEventMetric:metric];
314 secnotice("ckksfetch", "Metric submitted: %@", metric);
316 [launchMetricOp addSuccessDependency:recordZoneChangesCompletedOperation];
318 [strongSelf.operationQueue addOperation:launchMetricOp];
322 // Don't need these any more; save some memory
323 [strongSelf.modifications removeAllObjects];
324 [strongSelf.deletions removeAllObjects];
326 // Trigger the fake 'we're done' operation.
327 [strongSelf runBeforeGroupFinished: recordZoneChangesCompletedOperation];
330 [self dependOnBeforeGroupFinished:recordZoneChangesCompletedOperation];
331 [self dependOnBeforeGroupFinished:self.fetchRecordZoneChangesOperation];
333 [self.container.privateCloudDatabase addOperation:self.fetchRecordZoneChangesOperation];
337 [self.fetchRecordZoneChangesOperation cancel];