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@
 
  26 #import "SFAnalyticsSQLiteStore.h"
 
  27 #import "NSDate+SFAnalytics.h"
 
  28 #import "Analytics/SFAnalyticsDefines.h"
 
  29 #import "utilities/debugging.h"
 
  31 NSString* const SFAnalyticsColumnEventType = @"event_type";
 
  32 NSString* const SFAnalyticsColumnDate = @"timestamp";
 
  33 NSString* const SFAnalyticsColumnData = @"data";
 
  34 NSString* const SFAnalyticsUploadDate = @"upload_date";
 
  36 @implementation SFAnalyticsSQLiteStore
 
  38 + (instancetype)storeWithPath:(NSString*)path schema:(NSString*)schema
 
  41         seccritical("Cannot init db with empty path");
 
  44     if (![schema length]) {
 
  45         seccritical("Cannot init db without schema");
 
  49     SFAnalyticsSQLiteStore* store = nil;
 
  50     @synchronized([SFAnalyticsSQLiteStore class]) {
 
  51         static NSMutableDictionary* loggingStores = nil;
 
  52         static dispatch_once_t onceToken;
 
  53         dispatch_once(&onceToken, ^{
 
  54             loggingStores = [[NSMutableDictionary alloc] init];
 
  57         NSString* standardizedPath = path.stringByStandardizingPath;
 
  58         store = loggingStores[standardizedPath];
 
  60             store = [[self alloc] initWithPath:standardizedPath schema:schema];
 
  61             loggingStores[standardizedPath] = store;
 
  64         if (![store openWithError:&error] && !(error && error.code == SQLITE_AUTH)) {
 
  65             secerror("SFAnalytics: could not open db at init, will try again later. Error: %@", error);
 
  77 - (BOOL)tryToOpenDatabase
 
  81         if (![self openWithError:&error]) {
 
  84         secnotice("SFAnalytics", "successfully opened analytics db");
 
  89 - (NSInteger)successCountForEventType:(NSString*)eventType
 
  91     if (![self tryToOpenDatabase]) {
 
  94     return [[[[self select:@[SFAnalyticsColumnSuccessCount] from:SFAnalyticsTableSuccessCount where:@"event_type = ?" bindings:@[eventType]] firstObject] valueForKey:SFAnalyticsColumnSuccessCount] integerValue];
 
  97 - (void)incrementSuccessCountForEventType:(NSString*)eventType
 
  99     if (![self tryToOpenDatabase]) {
 
 102     NSInteger successCount = [self successCountForEventType:eventType];
 
 103     NSInteger hardFailureCount = [self hardFailureCountForEventType:eventType];
 
 104     NSInteger softFailureCount = [self softFailureCountForEventType:eventType];
 
 105     [self insertOrReplaceInto:SFAnalyticsTableSuccessCount values:@{SFAnalyticsColumnEventType : eventType, SFAnalyticsColumnSuccessCount : @(successCount + 1), SFAnalyticsColumnHardFailureCount : @(hardFailureCount), SFAnalyticsColumnSoftFailureCount : @(softFailureCount)}];
 
 108 - (NSInteger)hardFailureCountForEventType:(NSString*)eventType
 
 110     if (![self tryToOpenDatabase]) {
 
 113     return [[[[self select:@[SFAnalyticsColumnHardFailureCount] from:SFAnalyticsTableSuccessCount where:@"event_type = ?" bindings:@[eventType]] firstObject] valueForKey:SFAnalyticsColumnHardFailureCount] integerValue];
 
 116 - (NSInteger)softFailureCountForEventType:(NSString*)eventType
 
 118     if (![self tryToOpenDatabase]) {
 
 121     return [[[[self select:@[SFAnalyticsColumnSoftFailureCount] from:SFAnalyticsTableSuccessCount where:@"event_type = ?" bindings:@[eventType]] firstObject] valueForKey:SFAnalyticsColumnSoftFailureCount] integerValue];
 
 124 - (void)incrementHardFailureCountForEventType:(NSString*)eventType
 
 126     if (![self tryToOpenDatabase]) {
 
 129     NSInteger successCount = [self successCountForEventType:eventType];
 
 130     NSInteger hardFailureCount = [self hardFailureCountForEventType:eventType];
 
 131     NSInteger softFailureCount = [self softFailureCountForEventType:eventType];
 
 132     [self insertOrReplaceInto:SFAnalyticsTableSuccessCount values:@{SFAnalyticsColumnEventType : eventType, SFAnalyticsColumnSuccessCount : @(successCount), SFAnalyticsColumnHardFailureCount : @(hardFailureCount + 1), SFAnalyticsColumnSoftFailureCount : @(softFailureCount)}];
 
 135 - (void)incrementSoftFailureCountForEventType:(NSString*)eventType
 
 137     if (![self tryToOpenDatabase]) {
 
 140     NSInteger successCount = [self successCountForEventType:eventType];
 
 141     NSInteger hardFailureCount = [self hardFailureCountForEventType:eventType];
 
 142     NSInteger softFailureCount = [self softFailureCountForEventType:eventType];
 
 143     [self insertOrReplaceInto:SFAnalyticsTableSuccessCount values:@{SFAnalyticsColumnEventType : eventType, SFAnalyticsColumnSuccessCount : @(successCount), SFAnalyticsColumnHardFailureCount : @(hardFailureCount), SFAnalyticsColumnSoftFailureCount : @(softFailureCount + 1)}];
 
 146 - (NSDictionary*)summaryCounts
 
 148     if (![self tryToOpenDatabase]) {
 
 149         return [NSDictionary new];
 
 151     NSMutableDictionary* successCountsDict = [NSMutableDictionary dictionary];
 
 152     NSArray* rows = [self selectAllFrom:SFAnalyticsTableSuccessCount where:nil bindings:nil];
 
 153     for (NSDictionary* rowDict in rows) {
 
 154         NSString* eventName = rowDict[SFAnalyticsColumnEventType];
 
 156             secinfo("SFAnalytics", "ignoring entry in success counts table without an event name");
 
 160         successCountsDict[eventName] = @{SFAnalyticsTableSuccessCount : rowDict[SFAnalyticsColumnSuccessCount], SFAnalyticsColumnHardFailureCount : rowDict[SFAnalyticsColumnHardFailureCount], SFAnalyticsColumnSoftFailureCount : rowDict[SFAnalyticsColumnSoftFailureCount]};
 
 163     return successCountsDict;
 
 166 - (NSArray*)deserializedRecords:(NSArray*)recordBlobs
 
 168     if (![self tryToOpenDatabase]) {
 
 169         return [NSArray new];
 
 171     NSMutableArray* records = [NSMutableArray new];
 
 172     for (NSDictionary* row in recordBlobs) {
 
 173         NSMutableDictionary* deserializedRecord = [NSPropertyListSerialization propertyListWithData:row[SFAnalyticsColumnData] options:NSPropertyListMutableContainers format:nil error:nil];
 
 174         [records addObject:deserializedRecord];
 
 179 - (NSArray*)hardFailures
 
 181     if (![self tryToOpenDatabase]) {
 
 182         return [NSArray new];
 
 184     return [self deserializedRecords:[self select:@[SFAnalyticsColumnData] from:SFAnalyticsTableHardFailures]];
 
 187 - (NSArray*)softFailures
 
 189     if (![self tryToOpenDatabase]) {
 
 190         return [NSArray new];
 
 192     return [self deserializedRecords:[self select:@[SFAnalyticsColumnData] from:SFAnalyticsTableSoftFailures]];
 
 195 - (NSArray*)allEvents
 
 197     if (![self tryToOpenDatabase]) {
 
 198         return [NSArray new];
 
 203     NSMutableArray<NSDictionary *> *all = [NSMutableArray new];
 
 205     NSArray<NSDictionary *> *hard = [self select:@[SFAnalyticsColumnDate, SFAnalyticsColumnData] from:SFAnalyticsTableHardFailures];
 
 206     [all addObjectsFromArray:hard];
 
 209     NSArray<NSDictionary *> *soft = [self select:@[SFAnalyticsColumnDate, SFAnalyticsColumnData] from:SFAnalyticsTableSoftFailures];
 
 210     [all addObjectsFromArray:soft];
 
 213     NSArray<NSDictionary *> *notes = [self select:@[SFAnalyticsColumnDate, SFAnalyticsColumnData] from:SFAnalyticsTableNotes];
 
 214     [all addObjectsFromArray:notes];
 
 219     [all sortUsingComparator:^NSComparisonResult(NSDictionary  *_Nonnull obj1, NSDictionary *_Nonnull obj2) {
 
 220         NSDate *date1 = obj1[SFAnalyticsColumnDate];
 
 221         NSDate *date2 = obj2[SFAnalyticsColumnDate];
 
 222         return [date1 compare:date2];
 
 224     return [self deserializedRecords:all];
 
 229     if (![self tryToOpenDatabase]) {
 
 230         return [NSArray new];
 
 232     return [self select:@[SFAnalyticsColumnSampleName, SFAnalyticsColumnSampleValue] from:SFAnalyticsTableSamples];
 
 235 - (void)addEventDict:(NSDictionary*)eventDict toTable:(NSString*)table timestampBucket:(SFAnalyticsTimestampBucket)bucket
 
 237     if (![self tryToOpenDatabase]) {
 
 241     NSTimeInterval timestamp = [[NSDate date] timeIntervalSince1970WithBucket:bucket];
 
 242     NSError* error = nil;
 
 243     NSData* serializedRecord = [NSPropertyListSerialization dataWithPropertyList:eventDict format:NSPropertyListBinaryFormat_v1_0 options:0 error:&error];
 
 244     if(!error && serializedRecord) {
 
 245         [self insertOrReplaceInto:table values:@{SFAnalyticsColumnDate : @(timestamp), SFAnalyticsColumnData : serializedRecord}];
 
 247     if(error && !serializedRecord) {
 
 248         secerror("Couldn't serialize failure record: %@", error);
 
 252 - (void)addEventDict:(NSDictionary*)eventDict toTable:(NSString*)table
 
 254     [self addEventDict:eventDict toTable:table timestampBucket:SFAnalyticsTimestampBucketSecond];
 
 257 - (void)addSample:(NSNumber*)value forName:(NSString*)name
 
 259     if (![self tryToOpenDatabase]) {
 
 262     [self insertOrReplaceInto:SFAnalyticsTableSamples values:@{SFAnalyticsColumnDate : @([[NSDate date] timeIntervalSince1970]), SFAnalyticsColumnSampleName : name, SFAnalyticsColumnSampleValue : value}];
 
 265 - (void)removeAllSamplesForName:(NSString*)name
 
 267     if (![self tryToOpenDatabase]) {
 
 270     [self deleteFrom:SFAnalyticsTableSamples where:[NSString stringWithFormat:@"name == '%@'", name] bindings:nil];
 
 273 - (NSDate*)uploadDate
 
 275     if (![self tryToOpenDatabase]) {
 
 276         return nil;     // In other cases return default object but nil is better here to avoid entering the upload flow
 
 278     return [self datePropertyForKey:SFAnalyticsUploadDate];
 
 281 - (void)setUploadDate:(NSDate*)uploadDate
 
 283     if (![self tryToOpenDatabase]) {
 
 286     [self setDateProperty:uploadDate forKey:SFAnalyticsUploadDate];
 
 291     if (![self tryToOpenDatabase]) {
 
 294     [self deleteFrom:SFAnalyticsTableSuccessCount where:@"event_type like ?" bindings:@[@"%"]];
 
 295     [self deleteFrom:SFAnalyticsTableHardFailures where:@"id >= 0" bindings:nil];
 
 296     [self deleteFrom:SFAnalyticsTableSoftFailures where:@"id >= 0" bindings:nil];
 
 297     [self deleteFrom:SFAnalyticsTableSamples where:@"id >= 0" bindings:nil];