/* * Copyright 2017 Google * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #import "FValidation.h" #import "FConstants.h" #import "FParsedUrl.h" #import "FTypedefs.h" // Have to escape: * ? + [ ( ) { } ^ $ | \ . / // See: https://developer.apple.com/library/mac/#documentation/Foundation/Reference/NSRegularExpression_Class/Reference/Reference.html NSString *const kInvalidPathCharacters = @"[].#$"; NSString *const kInvalidKeyCharacters = @"[].#$/"; @implementation FValidation + (void) validateFrom:(NSString *)fn writablePath:(FPath *)path { if([[path getFront] isEqualToString:kDotInfoPrefix]) { @throw [[NSException alloc] initWithName:@"WritablePathValidation" reason:[NSString stringWithFormat:@"(%@) failed to path %@: Can't modify data under %@", fn, [path description], kDotInfoPrefix] userInfo:nil]; } } + (void) validateFrom:(NSString*)fn knownEventType:(FIRDataEventType)event { switch (event) { case FIRDataEventTypeValue: case FIRDataEventTypeChildAdded: case FIRDataEventTypeChildChanged: case FIRDataEventTypeChildMoved: case FIRDataEventTypeChildRemoved: return; break; default: @throw [[NSException alloc] initWithName:@"KnownEventTypeValidation" reason:[NSString stringWithFormat:@"(%@) Unknown event type: %d", fn, (int) event] userInfo:nil]; break; } } + (BOOL) isValidPathString:(NSString *)pathString { static dispatch_once_t token; static NSCharacterSet *badPathChars = nil; dispatch_once(&token, ^{ badPathChars = [NSCharacterSet characterSetWithCharactersInString:kInvalidPathCharacters]; }); return pathString != nil && [pathString length] != 0 && [pathString rangeOfCharacterFromSet:badPathChars].location == NSNotFound; } + (void) validateFrom:(NSString *)fn validPathString:(NSString *)pathString { if(! [self isValidPathString:pathString]) { @throw [[NSException alloc] initWithName:@"InvalidPathValidation" reason:[NSString stringWithFormat:@"(%@) Must be a non-empty string and not contain '.' '#' '$' '[' or ']'", fn] userInfo:nil]; } } + (void) validateFrom:(NSString *)fn validRootPathString:(NSString *)pathString { static dispatch_once_t token; static NSRegularExpression *dotInfoRegex = nil; dispatch_once(&token, ^{ dotInfoRegex = [NSRegularExpression regularExpressionWithPattern:@"^\\/*\\.info(\\/|$)" options:0 error:nil]; }); NSString *tempPath = pathString; // HACK: Obj-C regex are kinda' slow. Do a plain string search first before bothering with the regex. if ([pathString rangeOfString:@".info"].location != NSNotFound) { tempPath = [dotInfoRegex stringByReplacingMatchesInString:pathString options:0 range:NSMakeRange(0, pathString.length) withTemplate:@"/"]; } [self validateFrom:fn validPathString:tempPath]; } + (BOOL) isValidKey:(NSString *)key { static dispatch_once_t token; static NSCharacterSet *badKeyChars = nil; dispatch_once(&token, ^{ badKeyChars = [NSCharacterSet characterSetWithCharactersInString:kInvalidKeyCharacters]; }); return key != nil && key.length > 0 && [key rangeOfCharacterFromSet:badKeyChars].location == NSNotFound; } + (void) validateFrom:(NSString *)fn validKey:(NSString *)key { if (![self isValidKey:key]) { @throw [[NSException alloc] initWithName:@"InvalidKeyValidation" reason:[NSString stringWithFormat:@"(%@) Must be a non-empty string and not contain '/' '.' '#' '$' '[' or ']'", fn] userInfo:nil]; } } + (void) validateFrom:(NSString *)fn validURL:(FParsedUrl *)parsedUrl { NSString* pathString = [parsedUrl.path description]; [self validateFrom:fn validRootPathString:pathString]; } #pragma mark - #pragma mark Authentication validation + (BOOL) stringNonempty:(NSString *)str { return str != nil && ![str isKindOfClass:[NSNull class]] && str.length > 0; } + (void) validateToken:(NSString *)token { if (![FValidation stringNonempty:token]) { [NSException raise:NSInvalidArgumentException format:@"Can't have empty string or nil for custom token"]; } } #pragma mark - #pragma mark Handling authentication errors /** * This function immediately calls the callback. * It assumes that it is not on FirebaseWorker thread. * It assumes it's on a user-controlled thread. */ + (void) handleError:(NSError *)error withUserCallback:(fbt_void_nserror_id)userCallback { if (userCallback) { userCallback(error, nil); } } /** * This function immediately calls the callback. * It assumes that it is not on FirebaseWorker thread. * It assumes it's on a user-controlled thread. */ + (void) handleError:(NSError *)error withSuccessCallback:(fbt_void_nserror)userCallback { if (userCallback) { userCallback(error); } } #pragma mark - #pragma mark Snapshot validation + (BOOL) validateFrom:(NSString*)fn isValidLeafValue:(id)value withPath:(NSArray*)path { if ([value isKindOfClass:[NSString class]]) { // Try to avoid conversion to bytes if possible NSString* theString = value; if ([theString maximumLengthOfBytesUsingEncoding:NSUTF8StringEncoding] > kFirebaseMaxLeafSize && [theString lengthOfBytesUsingEncoding:NSUTF8StringEncoding] > kFirebaseMaxLeafSize) { NSRange range; range.location = 0; range.length = MIN(path.count, 50); NSString* pathString = [[path subarrayWithRange:range] componentsJoinedByString:@"."]; @throw [[NSException alloc] initWithName:@"InvalidFirebaseData" reason:[NSString stringWithFormat:@"(%@) String exceeds max size of %u utf8 bytes: %@", fn, (int)kFirebaseMaxLeafSize, pathString] userInfo:nil]; } return YES; } else if ([value isKindOfClass:[NSNumber class]]) { // Cannot store NaN, but otherwise can store NSNumbers. if ([[NSDecimalNumber notANumber] isEqualToNumber:value]) { NSRange range; range.location = 0; range.length = MIN(path.count, 50); NSString* pathString = [[path subarrayWithRange:range] componentsJoinedByString:@"."]; @throw [[NSException alloc] initWithName:@"InvalidFirebaseData" reason:[NSString stringWithFormat:@"(%@) Cannot store NaN at path: %@.", fn, pathString] userInfo:nil]; } return YES; } else if ([value isKindOfClass:[NSDictionary class]]) { NSDictionary* dval = value; if (dval[kServerValueSubKey] != nil) { if ([dval count] > 1) { NSRange range; range.location = 0; range.length = MIN(path.count, 50); NSString* pathString = [[path subarrayWithRange:range] componentsJoinedByString:@"."]; @throw [[NSException alloc] initWithName:@"InvalidFirebaseData" reason:[NSString stringWithFormat:@"(%@) Cannot store other keys with server value keys.%@.", fn, pathString] userInfo:nil]; } return YES; } return NO; } else if (value == [NSNull null] || value == nil) { // Null is valid type to store at leaf return YES; } return NO; } + (NSString*) parseAndValidateKey:(id)keyId fromFunction:(NSString*)fn path:(NSArray*)path { if (![keyId isKindOfClass:[NSString class]]) { NSRange range; range.location = 0; range.length = MIN(path.count, 50); NSString* pathString = [[path subarrayWithRange:range] componentsJoinedByString:@"."]; @throw [[NSException alloc] initWithName:@"InvalidFirebaseData" reason:[NSString stringWithFormat:@"(%@) Non-string keys are not allowed in object at path: %@", fn, pathString] userInfo:nil]; } return (NSString*)keyId; } + (void) validateFrom:(NSString*)fn validDictionaryKey:(id)keyId withPath:(NSArray*)path { NSString *key = [self parseAndValidateKey:keyId fromFunction:fn path:path]; if (![key isEqualToString:kPayloadPriority] && ![key isEqualToString:kPayloadValue] && ![key isEqualToString:kServerValueSubKey] && ![FValidation isValidKey:key]) { NSRange range; range.location = 0; range.length = MIN(path.count, 50); NSString *pathString = [[path subarrayWithRange:range] componentsJoinedByString:@"."]; @throw [[NSException alloc] initWithName:@"InvalidFirebaseData" reason:[NSString stringWithFormat:@"(%@) Invalid key in object at path: %@. Keys must be non-empty and cannot contain '/' '.' '#' '$' '[' or ']'", fn, pathString] userInfo:nil]; } } + (void) validateFrom:(NSString*)fn validUpdateDictionaryKey:(id)keyId withValue:(id)value { FPath *path = [FPath pathWithString:[self parseAndValidateKey:keyId fromFunction:fn path:@[]]]; __block NSInteger keyNum = 0; [path enumerateComponentsUsingBlock:^void (NSString *key, BOOL *stop) { if ([key isEqualToString:kPayloadPriority] && keyNum == [path length] - 1) { [self validateFrom:fn isValidPriorityValue:value withPath:@[]]; } else { keyNum++; if (![FValidation isValidKey:key]) { @throw [[NSException alloc] initWithName:@"InvalidFirebaseData" reason:[NSString stringWithFormat:@"(%@) Invalid key in object. Keys must be non-empty and cannot contain '.' '#' '$' '[' or ']'", fn] userInfo:nil]; } } }]; } + (void) validateFrom:(NSString*)fn isValidPriorityValue:(id)value withPath:(NSArray*)path { [self validateFrom:fn isValidPriorityValue:value withPath:path throwError:YES]; } /** * Returns YES if priority is valid. */ + (BOOL)validatePriorityValue:value { return [self validateFrom:nil isValidPriorityValue:value withPath:nil throwError:NO]; } /** * Helper for validating priorities. If passed YES for throwError, it'll throw descriptive errors on validation * problems. Else, it'll just return YES/NO. */ + (BOOL) validateFrom:(NSString*)fn isValidPriorityValue:(id)value withPath:(NSArray*)path throwError:(BOOL)throwError { if ([value isKindOfClass:[NSNumber class]]) { if ([[NSDecimalNumber notANumber] isEqualToNumber:value]) { if (throwError) { NSRange range; range.location = 0; range.length = MIN(path.count, 50); NSString *pathString = [[path subarrayWithRange:range] componentsJoinedByString:@"."]; @throw [[NSException alloc] initWithName:@"InvalidFirebaseData" reason:[NSString stringWithFormat:@"(%@) Cannot store NaN as priority at path: %@.", fn, pathString] userInfo:nil]; } else { return NO; } } else if (value == (id) kCFBooleanFalse || value == (id) kCFBooleanTrue) { if (throwError) { NSRange range; range.location = 0; range.length = MIN(path.count, 50); NSString *pathString = [[path subarrayWithRange:range] componentsJoinedByString:@"."]; @throw [[NSException alloc] initWithName:@"InvalidFirebaseData" reason:[NSString stringWithFormat:@"(%@) Cannot store true/false as priority at path: %@.", fn, pathString] userInfo:nil]; } else { return NO; } } } else if ([value isKindOfClass:[NSDictionary class]]) { NSDictionary *dval = value; if (dval[kServerValueSubKey] != nil) { if ([dval count] > 1) { if (throwError) { NSRange range; range.location = 0; range.length = MIN(path.count, 50); NSString *pathString = [[path subarrayWithRange:range] componentsJoinedByString:@"."]; @throw [[NSException alloc] initWithName:@"InvalidFirebaseData" reason:[NSString stringWithFormat:@"(%@) Cannot store other keys with server value keys as priority at path: %@.", fn, pathString] userInfo:nil]; } else { return NO; } } } else { if (throwError) { NSRange range; range.location = 0; range.length = MIN(path.count, 50); NSString *pathString = [[path subarrayWithRange:range] componentsJoinedByString:@"."]; @throw [[NSException alloc] initWithName:@"InvalidFirebaseData" reason:[NSString stringWithFormat:@"(%@) Cannot store an NSDictionary as priority at path: %@.", fn, pathString] userInfo:nil]; } else { return NO; } } } else if ([value isKindOfClass:[NSArray class]]) { if (throwError) { NSRange range; range.location = 0; range.length = MIN(path.count, 50); NSString *pathString = [[path subarrayWithRange:range] componentsJoinedByString:@"."]; @throw [[NSException alloc] initWithName:@"InvalidFirebaseData" reason:[NSString stringWithFormat:@"(%@) Cannot store an NSArray as priority at path: %@.", fn, pathString] userInfo:nil]; } else { return NO; } } // It's valid! return YES; } @end