Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,7 @@
7EB69F3B404D0AEF46EC1536 /* UserJwtLifecycleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BFE2F960129386AFA6D5F41 /* UserJwtLifecycleTests.swift */; };
E42087CB1AB15481E55D34FE /* UserStateReportingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDA6355B75EDC9A3100B98F5 /* UserStateReportingTests.swift */; };
9345FA2E8878EA1B0216A49A /* UserJwtAskTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3567F486AA4335CCA79D659B /* UserJwtAskTests.swift */; };
59611FB5A88143468425CCB3 /* CachedUserWithoutAnIdTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5249D3FD71F02D4BCAF0995B /* CachedUserWithoutAnIdTests.swift */; };
8D2F4893453206700BB60F85 /* OSOperationRepoTestSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5221EEDBA5A74BD565490D52 /* OSOperationRepoTestSupport.swift */; };
8E949FF4C7A7A2C7182E53EA /* OSUserJwtConfigTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9376A4957E9090C748BCB18 /* OSUserJwtConfigTests.swift */; };
911E2CBD1E398AB3003112A4 /* UnitTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 911E2CBC1E398AB3003112A4 /* UnitTests.m */; };
Expand Down Expand Up @@ -1607,6 +1608,7 @@
5BFE2F960129386AFA6D5F41 /* UserJwtLifecycleTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = UserJwtLifecycleTests.swift; sourceTree = "<group>"; };
FDA6355B75EDC9A3100B98F5 /* UserStateReportingTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = UserStateReportingTests.swift; sourceTree = "<group>"; };
3567F486AA4335CCA79D659B /* UserJwtAskTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = UserJwtAskTests.swift; sourceTree = "<group>"; };
5249D3FD71F02D4BCAF0995B /* CachedUserWithoutAnIdTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CachedUserWithoutAnIdTests.swift; sourceTree = "<group>"; };
6552F2A6DF7776B0582CFAEF /* OSUserJwtConfig.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = OSUserJwtConfig.swift; sourceTree = "<group>"; };
67ECA2928D863073B785F93F /* IamFetchIdentityVerificationTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = IamFetchIdentityVerificationTests.swift; sourceTree = "<group>"; };
6A8BBA843AFC81A4940CF7CC /* OSUserJwtRepo.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = OSUserJwtRepo.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2547,6 +2549,7 @@
5BFE2F960129386AFA6D5F41 /* UserJwtLifecycleTests.swift */,
FDA6355B75EDC9A3100B98F5 /* UserStateReportingTests.swift */,
3567F486AA4335CCA79D659B /* UserJwtAskTests.swift */,
5249D3FD71F02D4BCAF0995B /* CachedUserWithoutAnIdTests.swift */,
);
path = OneSignalUserTests;
sourceTree = "<group>";
Expand Down Expand Up @@ -4745,6 +4748,7 @@
7EB69F3B404D0AEF46EC1536 /* UserJwtLifecycleTests.swift in Sources */,
E42087CB1AB15481E55D34FE /* UserStateReportingTests.swift in Sources */,
9345FA2E8878EA1B0216A49A /* UserJwtAskTests.swift in Sources */,
59611FB5A88143468425CCB3 /* CachedUserWithoutAnIdTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
11 changes: 11 additions & 0 deletions iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCommonDefines.h
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,17 @@ typedef enum {GET, POST, HEAD, PUT, DELETE, OPTIONS, CONNECT, TRACE, PATCH} HTTP
#define OS_CUSTOM_EVENTS_EXECUTOR_DELTA_QUEUE_KEY @"OS_CUSTOM_EVENTS_EXECUTOR_DELTA_QUEUE_KEY"
#define OS_CUSTOM_EVENTS_EXECUTOR_REQUEST_QUEUE_KEY @"OS_CUSTOM_EVENTS_EXECUTOR_REQUEST_QUEUE_KEY"

// A codeable value larger than this is not written, and a cached queue blob larger than it is dropped without
// decoding it. Real queues are a few KB. CFPreferences rejects a value of 4 MB or more on iOS.
#define OS_USER_DEFAULTS_MAX_VALUE_BYTES 1048576

// Written only by the 5.3.0-beta Identity Verification builds. Nothing reads them anymore.
#define OS_IV_BETA_USER_EXECUTOR_PENDING_QUEUE_KEY @"OS_USER_EXECUTOR_PENDING_QUEUE_KEY"
#define OS_IV_BETA_IDENTITY_EXECUTOR_PENDING_QUEUE_KEY @"OS_IDENTITY_EXECUTOR_PENDING_QUEUE_KEY"
#define OS_IV_BETA_PROPERTIES_EXECUTOR_PENDING_QUEUE_KEY @"OS_PROPERTIES_EXECUTOR_PENDING_QUEUE_KEY"
#define OS_IV_BETA_SUBSCRIPTION_EXECUTOR_PENDING_QUEUE_KEY @"OS_SUBSCRIPTION_EXECUTOR_PENDING_QUEUE_KEY"
#define OS_IV_BETA_CUSTOM_EVENTS_EXECUTOR_PENDING_QUEUE_KEY @"OS_CUSTOM_EVENTS_EXECUTOR_PENDING_QUEUE_KEY"

// Live Activies Executor
#define OS_LIVE_ACTIVITIES_EXECUTOR_UPDATE_TOKENS_KEY @"OS_LIVE_ACTIVITIES_EXECUTOR_UPDATE_TOKENS_KEY"
#define OS_LIVE_ACTIVITIES_EXECUTOR_START_TOKENS_KEY @"OS_LIVE_ACTIVITIES_EXECUTOR_START_TOKENS_KEY"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,11 @@
- (void)saveObjectForKey:(NSString * _Nonnull)key withValue:(id _Nullable)value;

// NSUserDefaults for storing and getting saved codeable data (custom objects)
/// A blob that is not archived data or cannot be decoded is logged, left in place and `value` returned.
- (id _Nullable)getSavedCodeableDataForKey:(NSString * _Nonnull)key defaultValue:(id _Nullable)value;
/// Same, except that a blob larger than `maxBytes`, not archived data or undecodable is removed. 0 means no limit, and nothing is removed.
- (id _Nullable)getSavedCodeableDataForKey:(NSString * _Nonnull)key defaultValue:(id _Nullable)value maxBytes:(NSUInteger)maxBytes;
/// A value whose archive is larger than `OS_USER_DEFAULTS_MAX_VALUE_BYTES` is not written and the key is removed.
- (void)saveCodeableDataForKey:(NSString * _Nonnull)key withValue:(id _Nullable)value;

@end
63 changes: 57 additions & 6 deletions iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalUserDefaults.m
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
#import <Foundation/Foundation.h>
#import "OneSignalUserDefaults.h"
#import "OneSignalCommonDefines.h"
#import "OneSignalLog.h"

@implementation OneSignalUserDefaults : NSObject

Expand Down Expand Up @@ -152,15 +153,65 @@ - (void)saveObjectForKey:(NSString * _Nonnull)key withValue:(id _Nullable)object
}

- (id _Nullable)getSavedCodeableDataForKey:(NSString * _Nonnull)key defaultValue:(id _Nullable)value {
if ([self keyExists:key])
return [NSKeyedUnarchiver unarchiveObjectWithData:[self.userDefaults objectForKey:key]];

return value;
return [self getSavedCodeableDataForKey:key defaultValue:value maxBytes:0];
}

- (id _Nullable)getSavedCodeableDataForKey:(NSString * _Nonnull)key defaultValue:(id _Nullable)value maxBytes:(NSUInteger)maxBytes {
if (![self keyExists:key])
return value;

id stored = [self.userDefaults objectForKey:key];
if (![stored isKindOfClass:[NSData class]]) {
[OneSignalLog onesignalLog:ONE_S_LL_ERROR message:[NSString stringWithFormat:@"OneSignalUserDefaults cannot decode %@: not archived data", key]];
[self removeUndecodableValueForKey:key maxBytes:maxBytes];
return value;
}
NSData *data = stored;
if (maxBytes > 0 && data.length > maxBytes) {
[OneSignalLog onesignalLog:ONE_S_LL_ERROR message:[NSString stringWithFormat:@"OneSignalUserDefaults dropping %@: %lu bytes, limit %lu", key, (unsigned long)data.length, (unsigned long)maxBytes]];
[self removeValueForKey:key];
return value;
}
@try {
// A blob that decodes to nil is treated like a missing key.
return [NSKeyedUnarchiver unarchiveObjectWithData:data] ?: value;
} @catch (NSException *exception) {
[OneSignalLog onesignalLog:ONE_S_LL_ERROR message:[NSString stringWithFormat:@"OneSignalUserDefaults cannot decode %@: %@", key, exception.reason]];
[self removeUndecodableValueForKey:key maxBytes:maxBytes];
return value;
Comment thread
nan-li marked this conversation as resolved.
}
}

/// Only a capped read, a queue of this SDK's own Requests, heals by deletion. An uncapped read leaves the blob to
/// whichever module wrote it; a model store, for one, is overwritten by the user the SDK creates in its place.
- (void)removeUndecodableValueForKey:(NSString *)key maxBytes:(NSUInteger)maxBytes {
if (maxBytes > 0) {
[self removeValueForKey:key];
}
}

- (void)saveCodeableDataForKey:(NSString * _Nonnull)key withValue:(id _Nullable)value {
[self.userDefaults setObject:[NSKeyedArchiver archivedDataWithRootObject:value] forKey:key];
[self.userDefaults synchronize];
NSData *data;
@try {
data = [NSKeyedArchiver archivedDataWithRootObject:value];
} @catch (NSException *exception) {
[OneSignalLog onesignalLog:ONE_S_LL_ERROR message:[NSString stringWithFormat:@"OneSignalUserDefaults could not archive %@: %@", key, exception.reason]];
return;
}
// Nothing the SDK caches is legitimately this large: a queue that reached it is the bloat the read cap drops,
// so the previous blob goes too rather than resend the Requests it held. CFPreferences refuses a value of 4 MB
// or more and can stop persisting the suite afterwards.
if (data.length > OS_USER_DEFAULTS_MAX_VALUE_BYTES) {
[OneSignalLog onesignalLog:ONE_S_LL_ERROR message:[NSString stringWithFormat:@"OneSignalUserDefaults not caching %@: %lu bytes", key, (unsigned long)data.length]];
[self removeValueForKey:key];
Comment thread
nan-li marked this conversation as resolved.
return;
}
@try {
[self.userDefaults setObject:data forKey:key];
[self.userDefaults synchronize];
} @catch (NSException *exception) {
[OneSignalLog onesignalLog:ONE_S_LL_ERROR message:[NSString stringWithFormat:@"OneSignalUserDefaults could not cache %@: %@", key, exception.reason]];
}
}

//gets the NSBundle of the primary application - NOT the app extension
Expand Down
95 changes: 95 additions & 0 deletions iOS_SDK/OneSignalSDK/OneSignalCoreTests/OneSignalCoreTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -73,4 +73,99 @@ final class OneSignalCoreTests: XCTestCase {
XCTAssertEqual(templateId, "templateId123")
XCTAssertEqual(templateName, "Template name")
}

// MARK: - Codeable data guards

private let guardKey = "OS_CORE_TESTS_CODEABLE_GUARD"

func testOversizedCodeableDataIsDroppedNotDecoded() {
let defaults = OneSignalUserDefaults.initShared()
// A real archive over the cap; without the cap it would decode.
defaults.saveCodeableData(forKey: guardKey, withValue: [String(repeating: "x", count: 2048)])

let restored = defaults.getSavedCodeableData(forKey: guardKey, defaultValue: ["default"], maxBytes: 1024) as? [String]

XCTAssertEqual(restored, ["default"])
XCTAssertFalse(defaults.keyExists(guardKey))
}

func testCodeableDataWithinTheLimitIsDecoded() {
let defaults = OneSignalUserDefaults.initShared()
defaults.saveCodeableData(forKey: guardKey, withValue: ["cached"])

let restored = defaults.getSavedCodeableData(forKey: guardKey, defaultValue: [], maxBytes: 1024) as? [String]

XCTAssertEqual(restored, ["cached"])
defaults.removeValue(forKey: guardKey)
}

func testUnreadableCodeableDataReturnsTheDefaultWithoutThrowing() {
let defaults = OneSignalUserDefaults.initShared()
defaults.saveObject(forKey: guardKey, withValue: Data("not an archive".utf8))

let restored = defaults.getSavedCodeableData(forKey: guardKey, defaultValue: ["default"]) as? [String]

XCTAssertEqual(restored, ["default"])
defaults.removeValue(forKey: guardKey)
}

func testUnarchivableValueLeavesTheCachedValueInPlace() {
let defaults = OneSignalUserDefaults.initShared()
defaults.saveCodeableData(forKey: guardKey, withValue: ["cached"])

// NSObject does not adopt NSCoding, so archiving it raises.
defaults.saveCodeableData(forKey: guardKey, withValue: NSObject())

XCTAssertEqual(defaults.getSavedCodeableData(forKey: guardKey, defaultValue: nil) as? [String], ["cached"])
defaults.removeValue(forKey: guardKey)
}

func testValueOverTheLimitIsNotCachedAndThePreviousBlobGoesWithIt() {
let defaults = OneSignalUserDefaults.initShared()
defaults.saveCodeableData(forKey: guardKey, withValue: ["cached"])

defaults.saveCodeableData(forKey: guardKey, withValue: Data(count: Int(OS_USER_DEFAULTS_MAX_VALUE_BYTES)))

XCTAssertFalse(defaults.keyExists(guardKey))
}

func testBlobThatIsNotArchivedDataIsRemovedOnlyByACappedRead() {
let defaults = OneSignalUserDefaults.initShared()
defaults.saveObject(forKey: guardKey, withValue: "not data")

XCTAssertEqual(defaults.getSavedCodeableData(forKey: guardKey, defaultValue: ["default"]) as? [String], ["default"])
XCTAssertTrue(defaults.keyExists(guardKey))

XCTAssertEqual(defaults.getSavedCodeableData(forKey: guardKey, defaultValue: ["default"], maxBytes: 1024) as? [String], ["default"])
XCTAssertFalse(defaults.keyExists(guardKey))
}

func testUndecodableBlobIsRemovedOnlyByACappedRead() throws {
let defaults = OneSignalUserDefaults.initShared()
defaults.saveObject(forKey: guardKey, withValue: try archiveNamingAnUnknownClass())

XCTAssertEqual(defaults.getSavedCodeableData(forKey: guardKey, defaultValue: ["default"]) as? [String], ["default"])
XCTAssertTrue(defaults.keyExists(guardKey))

XCTAssertEqual(defaults.getSavedCodeableData(forKey: guardKey, defaultValue: ["default"], maxBytes: 1024) as? [String], ["default"])
XCTAssertFalse(defaults.keyExists(guardKey))
}

/// A keyed archive whose root is of a class this process does not have. Decoding it raises, as a queue written
/// by a newer build does.
private func archiveNamingAnUnknownClass() throws -> Data {
let archive = try NSKeyedArchiver.archivedData(withRootObject: ["cached"], requiringSecureCoding: false)
var plist = try XCTUnwrap(try PropertyListSerialization.propertyList(from: archive, format: nil) as? [String: Any])
var objects = try XCTUnwrap(plist["$objects"] as? [Any])
for (index, object) in objects.enumerated() {
guard var descriptor = object as? [String: Any], descriptor["$classname"] != nil else {
continue
}
descriptor["$classname"] = "OSNoSuchClass"
descriptor["$classes"] = ["OSNoSuchClass"]
objects[index] = descriptor
}
plist["$objects"] = objects
return try PropertyListSerialization.data(fromPropertyList: plist, format: .binary, options: 0)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ public class OSOperationRepo: NSObject {
}

private func uncacheDeltaQueue() {
guard let cached = OneSignalUserDefaults.initShared().getSavedCodeableData(forKey: OS_OPERATION_REPO_DELTA_QUEUE_KEY, defaultValue: []) as? [OSDelta] else {
guard let cached = OneSignalUserDefaults.initShared().getSavedCodeableData(forKey: OS_OPERATION_REPO_DELTA_QUEUE_KEY, defaultValue: [], maxBytes: UInt(OS_USER_DEFAULTS_MAX_VALUE_BYTES)) as? [OSDelta] else {
OneSignalLog.onesignalLog(.LL_ERROR, message: "OSOperationRepo is unable to uncache the OSDelta queue.")
return
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ class OSCustomEventsExecutor: OSOperationExecutor {
}

private func uncacheDeltas() {
if var deltaQueue = OneSignalUserDefaults.initShared().getSavedCodeableData(forKey: OS_CUSTOM_EVENTS_EXECUTOR_DELTA_QUEUE_KEY, defaultValue: []) as? [OSDelta] {
if var deltaQueue = OneSignalUserDefaults.initShared().getSavedCodeableData(forKey: OS_CUSTOM_EVENTS_EXECUTOR_DELTA_QUEUE_KEY, defaultValue: [], maxBytes: UInt(OS_USER_DEFAULTS_MAX_VALUE_BYTES)) as? [OSDelta] {
for (index, delta) in deltaQueue.enumerated().reversed() {
if OneSignalUserManagerImpl.sharedInstance.getIdentityModel(delta.identityModelId) == nil {
// The identity model does not exist, drop this Delta
Expand All @@ -81,7 +81,7 @@ class OSCustomEventsExecutor: OSOperationExecutor {
}

private func uncacheRequests() {
if var requestQueue = OneSignalUserDefaults.initShared().getSavedCodeableData(forKey: OS_CUSTOM_EVENTS_EXECUTOR_REQUEST_QUEUE_KEY, defaultValue: []) as? [OSRequestCustomEvents] {
if var requestQueue = OneSignalUserDefaults.initShared().getSavedCodeableData(forKey: OS_CUSTOM_EVENTS_EXECUTOR_REQUEST_QUEUE_KEY, defaultValue: [], maxBytes: UInt(OS_USER_DEFAULTS_MAX_VALUE_BYTES)) as? [OSRequestCustomEvents] {
// Hook each uncached Request to the model in the store
for (index, request) in requestQueue.enumerated().reversed() {
if let identityModel = OneSignalUserManagerImpl.sharedInstance.getIdentityModel(request.identityModel.modelId) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ class OSIdentityOperationExecutor: OSOperationExecutor {
}

private func uncacheDeltas() {
if var deltaQueue = OneSignalUserDefaults.initShared().getSavedCodeableData(forKey: OS_IDENTITY_EXECUTOR_DELTA_QUEUE_KEY, defaultValue: []) as? [OSDelta] {
if var deltaQueue = OneSignalUserDefaults.initShared().getSavedCodeableData(forKey: OS_IDENTITY_EXECUTOR_DELTA_QUEUE_KEY, defaultValue: [], maxBytes: UInt(OS_USER_DEFAULTS_MAX_VALUE_BYTES)) as? [OSDelta] {
// Hook each uncached Delta to the model in the store
for (index, delta) in deltaQueue.enumerated().reversed() {
if let modelInStore = OneSignalUserManagerImpl.sharedInstance.getIdentityModel(delta.model.modelId) {
Expand All @@ -70,7 +70,7 @@ class OSIdentityOperationExecutor: OSOperationExecutor {
}

private func uncacheAddAliasRequests() {
if var addRequestQueue = OneSignalUserDefaults.initShared().getSavedCodeableData(forKey: OS_IDENTITY_EXECUTOR_ADD_REQUEST_QUEUE_KEY, defaultValue: []) as? [OSRequestAddAliases] {
if var addRequestQueue = OneSignalUserDefaults.initShared().getSavedCodeableData(forKey: OS_IDENTITY_EXECUTOR_ADD_REQUEST_QUEUE_KEY, defaultValue: [], maxBytes: UInt(OS_USER_DEFAULTS_MAX_VALUE_BYTES)) as? [OSRequestAddAliases] {
// Hook each uncached Request to the model in the store
for (index, request) in addRequestQueue.enumerated().reversed() {
if let identityModel = OneSignalUserManagerImpl.sharedInstance.getIdentityModel(request.identityModel.modelId) {
Expand All @@ -93,7 +93,7 @@ class OSIdentityOperationExecutor: OSOperationExecutor {
}

private func uncacheRemoveAliasRequests() {
if var removeRequestQueue = OneSignalUserDefaults.initShared().getSavedCodeableData(forKey: OS_IDENTITY_EXECUTOR_REMOVE_REQUEST_QUEUE_KEY, defaultValue: []) as? [OSRequestRemoveAlias] {
if var removeRequestQueue = OneSignalUserDefaults.initShared().getSavedCodeableData(forKey: OS_IDENTITY_EXECUTOR_REMOVE_REQUEST_QUEUE_KEY, defaultValue: [], maxBytes: UInt(OS_USER_DEFAULTS_MAX_VALUE_BYTES)) as? [OSRequestRemoveAlias] {
// Hook each uncached Request to the model in the store
for (index, request) in removeRequestQueue.enumerated().reversed() {
if let identityModel = OneSignalUserManagerImpl.sharedInstance.getIdentityModel(request.identityModel.modelId) {
Expand Down
Loading
Loading