Skip to content

Commit 4e1f9bd

Browse files
feature(API): Adds input validation in all API methods and validates empty user Id. (#341)
* feature(API): Adds input validation in all API methods and validates empty user Id. * Addressed code review comments. * Addressed code review comments.
1 parent b848121 commit 4e1f9bd

File tree

7 files changed

+336
-60
lines changed

7 files changed

+336
-60
lines changed

OptimizelySDKCore/OptimizelySDKCore/OPTLYLoggerMessages.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,8 @@ extern NSString *const OPTLYLoggerMessagesGroupUnknownForGroupId;
204204
extern NSString *const OPTLYLoggerMessagesGetVariationNilVariation;
205205
extern NSString *const OPTLYLoggerMessagesVariationKeyUnknownForExperimentKey;
206206
extern NSString *const OPTLYLoggerMessagesProjectConfigUserIdInvalid;
207+
extern NSString *const OPTLYLoggerMessagesProjectConfigExperimentKeyInvalid;
208+
extern NSString *const OPTLYLoggerMessagesProjectConfigVariationKeyInvalid;
207209
extern NSString *const OPTLYLoggerMessagesAttributeIsReserved;
208210
extern NSString *const OPTLYLoggerMessagesAttributeNotFound;
209211

OptimizelySDKCore/OptimizelySDKCore/OPTLYLoggerMessages.m

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,9 @@
197197
NSString *const OPTLYLoggerMessagesGroupUnknownForGroupId = @"[PROJECT CONFIG] Group not found for group ID: %@. Group ID is not in the datafile."; // group id
198198
NSString *const OPTLYLoggerMessagesGetVariationNilVariation = @"[PROJECT CONFIG] Get variation returned a nil variation for user %@, experiment %@";
199199
NSString *const OPTLYLoggerMessagesVariationKeyUnknownForExperimentKey = @"[PROJECT CONFIG] Variation key %@ not found for experiment key %@.";
200-
NSString *const OPTLYLoggerMessagesProjectConfigUserIdInvalid = @"[PROJECT CONFIG] User ID cannot be an empty string.";
200+
NSString *const OPTLYLoggerMessagesProjectConfigUserIdInvalid = @"[PROJECT CONFIG] User ID cannot be nil.";
201+
NSString *const OPTLYLoggerMessagesProjectConfigExperimentKeyInvalid = @"[PROJECT CONFIG] Experiment Key cannot be nil or an empty string.";
202+
NSString *const OPTLYLoggerMessagesProjectConfigVariationKeyInvalid = @"[PROJECT CONFIG] Variation key cannot be an empty string.";
201203
NSString *const OPTLYLoggerMessagesAttributeIsReserved = @"[PROJECT CONFIG] Attribute %@ unexpectedly has reserved prefix %@; using attribute ID instead of reserved attribute name.";
202204
NSString *const OPTLYLoggerMessagesAttributeNotFound = @"[PROJECT CONFIG] Attribute key %@ is not in datafile.";
203205

OptimizelySDKCore/OptimizelySDKCore/OPTLYProjectConfig.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ __attribute((deprecated("Use OPTLYProjectConfig initWithBuilder method instead."
165165
*/
166166
- (BOOL)setForcedVariation:(nonnull NSString *)experimentKey
167167
userId:(nonnull NSString *)userId
168-
variationKey:(nonnull NSString *)variationKey;
168+
variationKey:(nullable NSString *)variationKey;
169169

170170
/**
171171
* Get variation for experiment and user ID with user attributes.

OptimizelySDKCore/OptimizelySDKCore/OPTLYProjectConfig.m

Lines changed: 56 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -308,74 +308,89 @@ - (OPTLYVariable *)getVariableForVariableKey:(NSString *)variableKey {
308308

309309
- (OPTLYVariation *)getForcedVariation:(nonnull NSString *)experimentKey
310310
userId:(nonnull NSString *)userId {
311+
312+
NSMutableDictionary<NSString *, NSString *> *dictionary = self.forcedVariationMap[userId];
313+
if (dictionary == nil) {
314+
return nil;
315+
}
311316
// Get experiment from experimentKey .
312317
OPTLYExperiment *experiment = [self getExperimentForKey:experimentKey];
313-
if (!experiment) {
314-
// NOTE: getExperimentForKey: will log any non-existent experimentKey and return experiment == nil for us.
318+
// this case is logged in getExperimentFromKey
319+
if (!experiment || [self isNullOrEmpty:experiment.experimentId]) {
315320
return nil;
316321
}
322+
317323
OPTLYVariation *variation = nil;
318324
@synchronized (self.forcedVariationMap) {
319-
NSMutableDictionary<NSString *, NSString *> *dictionary = self.forcedVariationMap[userId];
320-
if (dictionary != nil) {
321-
// Get variation from experimentId and variationId .
322-
NSString *experimentId = experiment.experimentId;
323-
NSString *variationId = dictionary[experimentId];
324-
variation = [experiment getVariationForVariationId:variationId];
325+
// Get variation from experimentId and variationId .
326+
NSString *variationId = dictionary[experiment.experimentId];
327+
if ([self isNullOrEmpty:variationId]) {
328+
return nil;
329+
}
330+
variation = [experiment getVariationForVariationId:variationId];
331+
332+
if (!variation || [self isNullOrEmpty:variation.variationKey]) {
333+
return nil;
325334
}
326335
}
336+
327337
return variation;
328338
}
329339

330340
- (BOOL)setForcedVariation:(nonnull NSString *)experimentKey
331341
userId:(nonnull NSString *)userId
332-
variationKey:(nonnull NSString *)variationKey {
342+
variationKey:(nullable NSString *)variationKey {
333343
// Return YES if there were no errors, OW return NO .
344+
//Check if variationKey is empty string
345+
if (variationKey != nil && [variationKey isEqualToString:@""]) {
346+
[self.logger logMessage:OPTLYLoggerMessagesProjectConfigVariationKeyInvalid withLevel:OptimizelyLogLevelDebug];
347+
return NO;
348+
}
349+
334350
// Get experiment from experimentKey .
335351
OPTLYExperiment *experiment = [self getExperimentForKey:experimentKey];
336352
if (!experiment) {
337353
// NOTE: getExperimentForKey: will log any non-existent experimentKey and return experiment == nil for us.
338354
return NO;
339355
}
340-
NSString *experimentId=experiment.experimentId;
341-
// Check for valid userId
342-
if ([userId length]==0) {
343-
[self.logger logMessage:OPTLYLoggerMessagesProjectConfigUserIdInvalid withLevel:OptimizelyLogLevelDebug];
356+
NSString *experimentId = experiment.experimentId;
357+
//Check if experimentId is valid
358+
if ([self isNullOrEmpty:experimentId]) {
344359
return NO;
345360
}
346-
// Get variation from experiment and non-nil variationKey, if applicable.
347-
OPTLYVariation *variation = nil;
348-
if (variationKey != nil) {
349-
variation = [experiment getVariationForVariationKey:variationKey];
350-
if (!variation) {
351-
NSString *logMessage = [NSString stringWithFormat:OPTLYLoggerMessagesVariationKeyUnknownForExperimentKey, variationKey, experimentKey];
352-
[self.logger logMessage:logMessage withLevel:OptimizelyLogLevelDebug];
353-
// Leave in current state, and report NO meaning there was an error.
354-
return NO;
361+
362+
@synchronized (self.forcedVariationMap) {
363+
// clear the forced variation if the variation key is null
364+
if (variationKey == nil) {
365+
// Locate relevant dictionary inside forcedVariationMap
366+
NSMutableDictionary<NSString *, NSString *> *dictionary = self.forcedVariationMap[userId];
367+
if (dictionary != nil) {
368+
[dictionary removeObjectForKey:experimentId];
369+
self.forcedVariationMap[userId] = dictionary;
370+
}
371+
return YES;
355372
}
356373
}
374+
375+
// Get variation from experiment and non-nil variationKey, if applicable.
376+
OPTLYVariation *variation = [experiment getVariationForVariationKey:variationKey];
377+
if (!variation || [self isNullOrEmpty:variation.variationId]) {
378+
NSString *logMessage = [NSString stringWithFormat:OPTLYLoggerMessagesVariationKeyUnknownForExperimentKey, variationKey, experimentKey];
379+
[self.logger logMessage:logMessage withLevel:OptimizelyLogLevelDebug];
380+
// Leave in current state, and report NO meaning there was an error.
381+
return NO;
382+
}
383+
357384
@synchronized (self.forcedVariationMap) {
358-
// Locate relevant dictionary inside forcedVariationMap
385+
// Add User if not exist.
359386
NSMutableDictionary<NSString *, NSString *> *dictionary = self.forcedVariationMap[userId];
360-
if ((dictionary == nil) && (variationKey != nil)) {
387+
if (dictionary == nil) {
361388
// We need a non-nil dictionary to store an OPTLYVariation .
362389
dictionary = [[NSMutableDictionary alloc] init];
363390
self.forcedVariationMap[userId] = dictionary;
364391
}
365-
// Apply change to dictionary
366-
if (variation == nil) {
367-
// NOTE: removeObjectForKey: "Does nothing if [experimentKey] does not exist."
368-
// https://developer.apple.com/documentation/foundation/nsmutabledictionary/1416518-removeobjectforkey?language=objc
369-
[dictionary removeObjectForKey:experimentId];
370-
if ([dictionary count] == 0) {
371-
// For elegance, we can remove empty dictionary for userId from self.forcedVariationMap
372-
[self.forcedVariationMap removeObjectForKey:userId];
373-
}
374-
} else {
375-
// If there is no OPTLYExperiment *experiment corresponding to experimentKey ,
376-
// then we will land in the "(variation == nil)" case above due to code above.
377-
dictionary[experimentId] = variation.variationId;
378-
};
392+
// Add/Replace Experiment to Variation ID map.
393+
self.forcedVariationMap[userId][experimentId] = variation.variationId;
379394
};
380395
return YES;
381396
}
@@ -623,5 +638,7 @@ - (OPTLYVariation *)getVariationForExperiment:(NSString *)experimentKey
623638
return bucketedVariation;
624639
}
625640

626-
641+
- (BOOL)isNullOrEmpty:(nullable NSString *)value {
642+
return ((value == nil) || [value isKindOfClass: [NSNull class]] || [value isEqualToString:@""]);
643+
}
627644
@end

OptimizelySDKCore/OptimizelySDKCore/Optimizely.m

Lines changed: 102 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,15 @@
4343
#import "OPTLYVariable.h"
4444
#import "OPTLYVariationVariable.h"
4545

46+
NSString *const OptimizelyNotificationsUserDictionaryExperimentKey = @"experiment";
47+
NSString *const OptimizelyNotificationsUserDictionaryVariationKey = @"variation";
48+
NSString *const OptimizelyNotificationsUserDictionaryUserIdKey = @"userId";
49+
NSString *const OptimizelyNotificationsUserDictionaryAttributesKey = @"attributes";
50+
NSString *const OptimizelyNotificationsUserDictionaryEventNameKey = @"eventKey";
51+
NSString *const OptimizelyNotificationsUserDictionaryFeatureKey = @"feature";
52+
NSString *const OptimizelyNotificationsUserDictionaryVariableKey = @"variable";
53+
NSString *const OptimizelyNotificationsUserDictionaryExperimentVariationMappingKey = @"ExperimentVariationMapping";
54+
4655
@implementation Optimizely
4756

4857
+ (instancetype)init:(OPTLYBuilderBlock)builderBlock {
@@ -186,27 +195,39 @@ - (OPTLYVariation *)variation:(NSString *)experimentKey
186195
#pragma mark Forced variation methods
187196
- (OPTLYVariation *)getForcedVariation:(nonnull NSString *)experimentKey
188197
userId:(nonnull NSString *)userId {
198+
NSMutableDictionary<NSString *, NSString *> *inputValues = [[NSMutableDictionary alloc] initWithDictionary:@{
199+
OptimizelyNotificationsUserDictionaryUserIdKey:[self ObjectOrNull:userId],
200+
OptimizelyNotificationsUserDictionaryExperimentKey:[self ObjectOrNull:experimentKey]}];
201+
if (![self validateStringInputs:inputValues logs:@{}]) {
202+
return nil;
203+
}
189204
return [self.config getForcedVariation:experimentKey
190205
userId:userId];
191206
}
192207

193208
- (BOOL)setForcedVariation:(nonnull NSString *)experimentKey
194209
userId:(nonnull NSString *)userId
195210
variationKey:(nullable NSString *)variationKey {
196-
return [self.config setForcedVariation:experimentKey
211+
NSMutableDictionary<NSString *, NSString *> *inputValues = [[NSMutableDictionary alloc] initWithDictionary:@{
212+
OptimizelyNotificationsUserDictionaryUserIdKey:[self ObjectOrNull:userId],
213+
OptimizelyNotificationsUserDictionaryExperimentKey:[self ObjectOrNull:experimentKey]}];
214+
return [self validateStringInputs:inputValues logs:@{}] && [self.config setForcedVariation:experimentKey
197215
userId:userId
198216
variationKey:variationKey];
199217
}
200218

201219
#pragma mark - Feature Flag Methods
202220

203221
- (BOOL)isFeatureEnabled:(NSString *)featureKey userId:(NSString *)userId attributes:(nullable NSDictionary<NSString *, NSObject *> *)attributes {
204-
if ([userId getValidString] == nil) {
205-
[self.logger logMessage:OPTLYLoggerMessagesFeatureDisabledUserIdInvalid withLevel:OptimizelyLogLevelError];
206-
return false;
207-
}
208-
if ([featureKey getValidString] == nil) {
209-
[self.logger logMessage:OPTLYLoggerMessagesFeatureDisabledFlagKeyInvalid withLevel:OptimizelyLogLevelError];
222+
223+
NSMutableDictionary<NSString *, NSString *> *inputValues = [[NSMutableDictionary alloc] initWithDictionary:@{
224+
OptimizelyNotificationsUserDictionaryUserIdKey:[self ObjectOrNull:userId],
225+
OptimizelyNotificationsUserDictionaryExperimentKey:[self ObjectOrNull:featureKey]}];
226+
NSDictionary <NSString *, NSString *> *logs = @{
227+
OptimizelyNotificationsUserDictionaryUserIdKey:OPTLYLoggerMessagesFeatureDisabledUserIdInvalid,
228+
OptimizelyNotificationsUserDictionaryExperimentKey:OPTLYLoggerMessagesFeatureDisabledFlagKeyInvalid};
229+
230+
if (![self validateStringInputs:inputValues logs:logs]) {
210231
return false;
211232
}
212233

@@ -246,16 +267,17 @@ - (NSString *)getFeatureVariableValueForType:(NSString *)variableType
246267
variableKey:(nullable NSString *)variableKey
247268
userId:(nullable NSString *)userId
248269
attributes:(nullable NSDictionary<NSString *, NSObject *> *)attributes {
249-
if ([featureKey getValidString] == nil) {
250-
[self.logger logMessage:OPTLYLoggerMessagesFeatureVariableValueFlagKeyInvalid withLevel:OptimizelyLogLevelError];
251-
return nil;
252-
}
253-
if ([variableKey getValidString] == nil) {
254-
[self.logger logMessage:OPTLYLoggerMessagesFeatureVariableValueVariableKeyInvalid withLevel:OptimizelyLogLevelError];
255-
return nil;
256-
}
257-
if ([userId getValidString] == nil) {
258-
[self.logger logMessage:OPTLYLoggerMessagesFeatureVariableValueUserIdInvalid withLevel:OptimizelyLogLevelError];
270+
271+
NSMutableDictionary<NSString *, NSString *> *inputValues = [[NSMutableDictionary alloc] initWithDictionary:@{
272+
OptimizelyNotificationsUserDictionaryUserIdKey:[self ObjectOrNull:userId],
273+
OptimizelyNotificationsUserDictionaryFeatureKey:[self ObjectOrNull:featureKey],
274+
OptimizelyNotificationsUserDictionaryVariableKey:[self ObjectOrNull:variableKey]}];
275+
NSDictionary <NSString *, NSString *> *logs = @{
276+
OptimizelyNotificationsUserDictionaryUserIdKey:OPTLYLoggerMessagesFeatureVariableValueUserIdInvalid,
277+
OptimizelyNotificationsUserDictionaryVariableKey:OPTLYLoggerMessagesFeatureVariableValueVariableKeyInvalid,
278+
OptimizelyNotificationsUserDictionaryFeatureKey:OPTLYLoggerMessagesFeatureVariableValueFlagKeyInvalid};
279+
280+
if (![self validateStringInputs:inputValues logs:logs]) {
259281
return nil;
260282
}
261283

@@ -364,7 +386,17 @@ - (NSString *)getFeatureVariableString:(nullable NSString *)featureKey
364386
-(NSArray<NSString *> *)getEnabledFeatures:(NSString *)userId
365387
attributes:(NSDictionary<NSString *, NSObject *> *)attributes {
366388

389+
367390
NSMutableArray<NSString *> *enabledFeatures = [NSMutableArray new];
391+
392+
NSMutableDictionary<NSString *, NSString *> *inputValues = [[NSMutableDictionary alloc] initWithDictionary:@{
393+
OptimizelyNotificationsUserDictionaryUserIdKey:[self ObjectOrNull:userId]}];
394+
NSDictionary <NSString *, NSString *> *logs = @{};
395+
396+
if (![self validateStringInputs:inputValues logs:logs]) {
397+
return enabledFeatures;
398+
}
399+
368400
for (OPTLYFeatureFlag *feature in self.config.featureFlags) {
369401
NSString *featureKey = feature.key;
370402
if ([self isFeatureEnabled:featureKey userId:userId attributes:attributes]) {
@@ -927,4 +959,57 @@ - (OPTLYVariation *)sendImpressionEventFor:(OPTLYExperiment *)experiment
927959
}
928960
return decisions;
929961
}
962+
963+
+ (BOOL)isEmptyArray:(NSObject*)array {
964+
return (!array
965+
|| ![array isKindOfClass:[NSArray class]]
966+
|| (((NSArray *)array).count == 0));
967+
}
968+
969+
+ (BOOL)isEmptyString:(NSObject*)string {
970+
return (!string
971+
|| ![string isKindOfClass:[NSString class]]
972+
|| [(NSString *)string isEqualToString:@""]);
973+
}
974+
975+
+ (BOOL)isEmptyDictionary:(NSObject*)dict {
976+
return (!dict
977+
|| ![dict isKindOfClass:[NSDictionary class]]
978+
|| (((NSDictionary *)dict).count == 0));
979+
}
980+
981+
+ (NSString *)stringOrEmpty:(NSString *)str {
982+
NSString *string = str != nil ? str : @"";
983+
return string;
984+
}
985+
986+
- (BOOL)validateStringInputs:(NSMutableDictionary<NSString *, NSString *> *)inputs logs:(NSDictionary<NSString *, NSString *> *)logs {
987+
NSMutableDictionary *_inputs = [inputs mutableCopy];
988+
BOOL __block isValid = true;
989+
// Empty user Id is valid value.
990+
if (_inputs.allKeys.count > 0) {
991+
if ([_inputs.allKeys containsObject:OptimizelyNotificationsUserDictionaryUserIdKey]) {
992+
if ([[_inputs objectForKey:OptimizelyNotificationsUserDictionaryUserIdKey] isKindOfClass:[NSNull class]]) {
993+
isValid = false;
994+
if ([logs objectForKey:OptimizelyNotificationsUserDictionaryUserIdKey]) {
995+
[self.logger logMessage:[logs objectForKey:OptimizelyNotificationsUserDictionaryUserIdKey] withLevel:OptimizelyLogLevelError];
996+
}
997+
}
998+
[_inputs removeObjectForKey:OptimizelyNotificationsUserDictionaryUserIdKey];
999+
}
1000+
}
1001+
[_inputs enumerateKeysAndObjectsUsingBlock:^(id key, id value, BOOL* stop) {
1002+
if ([value isKindOfClass:[NSNull class]] || [value isEqualToString:@""]) {
1003+
if ([logs objectForKey:key]) {
1004+
[self.logger logMessage:[logs objectForKey:key] withLevel:OptimizelyLogLevelError];
1005+
}
1006+
isValid = false;
1007+
}
1008+
}];
1009+
return isValid;
1010+
}
1011+
1012+
- (id)ObjectOrNull:(id)object {
1013+
return object ?: [NSNull null];
1014+
}
9301015
@end

OptimizelySDKCore/OptimizelySDKCoreTests/OPTLYDecisionServiceTest.m

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -471,7 +471,7 @@ - (void)testSetForcedVariationCalledOnInvalidVariationKey2
471471
- (void)testSetForcedVariationCalledOnInvalidUserId
472472
{
473473
NSString *invalidUserId = @"";
474-
XCTAssertFalse([self.optimizely setForcedVariation:kExperimentNotRunningKey
474+
XCTAssertTrue([self.optimizely setForcedVariation:kExperimentNotRunningKey
475475
userId:invalidUserId
476476
variationKey:kExperimentNoAudienceVariationKey]);
477477
}

0 commit comments

Comments
 (0)