Skip to content
Open
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
12 changes: 12 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,18 @@ 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 cached queue past this size is dropped without decoding it. Real queues are a few KB.
#define OS_CACHED_QUEUE_MAX_BYTES 1048576
// CFPreferences rejects a single value at or above this size on iOS.
#define OS_USER_DEFAULTS_MAX_VALUE_BYTES 4194304

// 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 @@ -70,6 +70,8 @@

// NSUserDefaults for storing and getting saved codeable data (custom objects)
- (id _Nullable)getSavedCodeableDataForKey:(NSString * _Nonnull)key defaultValue:(id _Nullable)value;
/// Removes a stored blob larger than `maxBytes` and returns `value` instead of decoding it.
- (id _Nullable)getSavedCodeableDataForKey:(NSString * _Nonnull)key defaultValue:(id _Nullable)value maxBytes:(NSUInteger)maxBytes;
- (void)saveCodeableDataForKey:(NSString * _Nonnull)key withValue:(id _Nullable)value;

@end
55 changes: 49 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,57 @@ - (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 dropping %@: not archived data", key]];
[self removeValueForKey:key];
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 dropping %@: %@", key, exception.reason]];
[self removeValueForKey:key];
return value;
}
}

- (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) {
// The old blob no longer matches memory, so drop it rather than restore it on the next launch.
[OneSignalLog onesignalLog:ONE_S_LL_ERROR message:[NSString stringWithFormat:@"OneSignalUserDefaults could not archive %@, removing it: %@", key, exception.reason]];
[self removeValueForKey:key];
return;
}
// CFPreferences refuses a value this large and may 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];
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
53 changes: 53 additions & 0 deletions iOS_SDK/OneSignalSDK/OneSignalCoreTests/OneSignalCoreTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -73,4 +73,57 @@ 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()
defaults.saveObject(forKey: guardKey, withValue: Data(count: 1025))

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 testUnarchivableValueIsNotCachedAndClearsTheKey() {
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())

XCTAssertFalse(defaults.keyExists(guardKey))
}

func testValueAtThePlatformLimitIsNotCached() {
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))
}
}
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_CACHED_QUEUE_MAX_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_CACHED_QUEUE_MAX_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_CACHED_QUEUE_MAX_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_CACHED_QUEUE_MAX_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_CACHED_QUEUE_MAX_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_CACHED_QUEUE_MAX_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
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ class OSPropertyOperationExecutor: OSOperationExecutor {
}

private func uncacheDeltas() {
if var deltaQueue = OneSignalUserDefaults.initShared().getSavedCodeableData(forKey: OS_PROPERTIES_EXECUTOR_DELTA_QUEUE_KEY, defaultValue: []) as? [OSDelta] {
if var deltaQueue = OneSignalUserDefaults.initShared().getSavedCodeableData(forKey: OS_PROPERTIES_EXECUTOR_DELTA_QUEUE_KEY, defaultValue: [], maxBytes: UInt(OS_CACHED_QUEUE_MAX_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 @@ -101,7 +101,7 @@ class OSPropertyOperationExecutor: OSOperationExecutor {
}

private func uncacheUpdateRequests() {
if var updateRequestQueue = OneSignalUserDefaults.initShared().getSavedCodeableData(forKey: OS_PROPERTIES_EXECUTOR_UPDATE_REQUEST_QUEUE_KEY, defaultValue: []) as? [OSRequestUpdateProperties] {
if var updateRequestQueue = OneSignalUserDefaults.initShared().getSavedCodeableData(forKey: OS_PROPERTIES_EXECUTOR_UPDATE_REQUEST_QUEUE_KEY, defaultValue: [], maxBytes: UInt(OS_CACHED_QUEUE_MAX_BYTES)) as? [OSRequestUpdateProperties] {
// Hook each uncached Request to the model in the store
for (index, request) in updateRequestQueue.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 @@ -53,7 +53,7 @@ class OSSubscriptionOperationExecutor: OSOperationExecutor {
}

private func uncacheDeltas() {
if var deltaQueue = OneSignalUserDefaults.initShared().getSavedCodeableData(forKey: OS_SUBSCRIPTION_EXECUTOR_DELTA_QUEUE_KEY, defaultValue: []) as? [OSDelta] {
if var deltaQueue = OneSignalUserDefaults.initShared().getSavedCodeableData(forKey: OS_SUBSCRIPTION_EXECUTOR_DELTA_QUEUE_KEY, defaultValue: [], maxBytes: UInt(OS_CACHED_QUEUE_MAX_BYTES)) as? [OSDelta] {
// Hook each uncached Delta to the model in the store
for (index, delta) in deltaQueue.enumerated().reversed() {
if let modelInStore = getSubscriptionModelFromStores(modelId: delta.model.modelId) {
Expand All @@ -75,7 +75,7 @@ class OSSubscriptionOperationExecutor: OSOperationExecutor {
private func uncacheCreateSubscriptionRequests() {
var requestQueue: [OSRequestCreateSubscription] = []

if let cachedAddRequestQueue = OneSignalUserDefaults.initShared().getSavedCodeableData(forKey: OS_SUBSCRIPTION_EXECUTOR_ADD_REQUEST_QUEUE_KEY, defaultValue: []) as? [OSRequestCreateSubscription] {
if let cachedAddRequestQueue = OneSignalUserDefaults.initShared().getSavedCodeableData(forKey: OS_SUBSCRIPTION_EXECUTOR_ADD_REQUEST_QUEUE_KEY, defaultValue: [], maxBytes: UInt(OS_CACHED_QUEUE_MAX_BYTES)) as? [OSRequestCreateSubscription] {
// Hook each uncached Request to the model in the store
for request in cachedAddRequestQueue {
// 1. Hook up the subscription model
Expand Down Expand Up @@ -111,7 +111,7 @@ class OSSubscriptionOperationExecutor: OSOperationExecutor {
}

private func uncacheDeleteSubscriptionRequests() {
if var removeRequestQueue = OneSignalUserDefaults.initShared().getSavedCodeableData(forKey: OS_SUBSCRIPTION_EXECUTOR_REMOVE_REQUEST_QUEUE_KEY, defaultValue: []) as? [OSRequestDeleteSubscription] {
if var removeRequestQueue = OneSignalUserDefaults.initShared().getSavedCodeableData(forKey: OS_SUBSCRIPTION_EXECUTOR_REMOVE_REQUEST_QUEUE_KEY, defaultValue: [], maxBytes: UInt(OS_CACHED_QUEUE_MAX_BYTES)) as? [OSRequestDeleteSubscription] {
// Hook each uncached Request to the model in the store
for (index, request) in removeRequestQueue.enumerated().reversed() {
if let subscriptionModel = getSubscriptionModelFromStores(modelId: request.subscriptionModel.modelId) {
Expand All @@ -136,7 +136,7 @@ class OSSubscriptionOperationExecutor: OSOperationExecutor {
}

private func uncacheUpdateSubscriptionRequests() {
if var updateRequestQueue = OneSignalUserDefaults.initShared().getSavedCodeableData(forKey: OS_SUBSCRIPTION_EXECUTOR_UPDATE_REQUEST_QUEUE_KEY, defaultValue: []) as? [OSRequestUpdateSubscription] {
if var updateRequestQueue = OneSignalUserDefaults.initShared().getSavedCodeableData(forKey: OS_SUBSCRIPTION_EXECUTOR_UPDATE_REQUEST_QUEUE_KEY, defaultValue: [], maxBytes: UInt(OS_CACHED_QUEUE_MAX_BYTES)) as? [OSRequestUpdateSubscription] {
// Hook each uncached Request to the model in the store
for (index, request) in updateRequestQueue.enumerated().reversed() {
if let subscriptionModel = getSubscriptionModelFromStores(modelId: request.subscriptionModel.modelId) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ class OSUserExecutor {
var userRequestQueue: [OSUserRequest] = []

// Read unfinished Create User + Identify User + Get Identity By Subscription requests from cache, if any...
if let cachedRequestQueue = OneSignalUserDefaults.initShared().getSavedCodeableData(forKey: OS_USER_EXECUTOR_USER_REQUEST_QUEUE_KEY, defaultValue: []) as? [OSUserRequest] {
if let cachedRequestQueue = OneSignalUserDefaults.initShared().getSavedCodeableData(forKey: OS_USER_EXECUTOR_USER_REQUEST_QUEUE_KEY, defaultValue: [], maxBytes: UInt(OS_CACHED_QUEUE_MAX_BYTES)) as? [OSUserRequest] {
// Hook each uncached Request to the right model reference
for request in cachedRequestQueue {
if request.isKind(of: OSRequestFetchIdentityBySubscription.self), let req = request as? OSRequestFetchIdentityBySubscription {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,23 @@ public class OneSignalUserManagerImpl: NSObject, OneSignalUserManager {
self.pushSubscriptionImpl = OSPushSubscriptionImpl(pushSubscriptionModelStore: pushSubscriptionModelStore)
}

/// The 5.3.0-beta builds parked Requests awaiting a JWT under these keys and never trimmed them, so an
/// upgrading device can carry a large blob nothing reads anymore.
private func removeIdentityVerificationBetaCaches() {
let defaults = OneSignalUserDefaults.initShared()
let keys = [
OS_IV_BETA_USER_EXECUTOR_PENDING_QUEUE_KEY,
OS_IV_BETA_IDENTITY_EXECUTOR_PENDING_QUEUE_KEY,
OS_IV_BETA_PROPERTIES_EXECUTOR_PENDING_QUEUE_KEY,
OS_IV_BETA_SUBSCRIPTION_EXECUTOR_PENDING_QUEUE_KEY,
OS_IV_BETA_CUSTOM_EVENTS_EXECUTOR_PENDING_QUEUE_KEY
]
for key in keys where defaults.keyExists(key) {
OneSignalLog.onesignalLog(.LL_DEBUG, message: "OneSignalUserManager removing the Identity Verification beta cache \(key)")
defaults.removeValue(forKey: key)
}
}

@objc
public func start() {
guard !OneSignalConfig.shouldAwaitAppIdAndLogMissingPrivacyConsent(forMethod: nil) else {
Expand Down Expand Up @@ -307,6 +324,8 @@ public class OneSignalUserManagerImpl: NSObject, OneSignalUserManager {
self?.pushSubscriptionModelStore.getModel(key: OS_PUSH_SUBSCRIPTION_MODEL_KEY)?._isDisabledInternally = false
}

removeIdentityVerificationBetaCaches()

// Setup the executors
// The OSUserExecutor has to run first, before other executors
self.userExecutor = OSUserExecutor(newRecordsState: newRecordsState, identityVerificationService: identityVerificationService, auth: requestAuth)
Expand Down
Loading
Loading