Files
firebase-ios-sdk/FirebaseDatabase/Sources/Utilities/FValidation.m
T

461 lines
17 KiB
Objective-C
Raw Normal View History

2017-05-15 12:27:07 -07:00
/*
* 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.
*/
2020-06-22 07:58:52 -07:00
#import "FirebaseDatabase/Sources/Utilities/FValidation.h"
#import "FirebaseDatabase/Sources/Constants/FConstants.h"
#import "FirebaseDatabase/Sources/Utilities/FParsedUrl.h"
#import "FirebaseDatabase/Sources/Utilities/FTypedefs.h"
2017-05-15 12:27:07 -07:00
// Have to escape: * ? + [ ( ) { } ^ $ | \ . /
2019-08-05 13:27:47 -07:00
// See:
// https://developer.apple.com/library/mac/#documentation/Foundation/Reference/NSRegularExpression_Class/Reference/Reference.html
2017-05-15 12:27:07 -07:00
NSString *const kInvalidPathCharacters = @"[].#$";
2019-08-05 13:27:47 -07:00
NSString *const kInvalidKeyCharacters = @"[].#$/";
2017-05-15 12:27:07 -07:00
@implementation FValidation
2019-08-05 13:27:47 -07:00
+ (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];
2017-05-15 12:27:07 -07:00
}
}
2019-08-05 13:27:47 -07:00
+ (void)validateFrom:(NSString *)fn knownEventType:(FIRDataEventType)event {
2017-05-15 12:27:07 -07:00
switch (event) {
2019-08-05 13:27:47 -07:00
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;
2017-05-15 12:27:07 -07:00
}
}
2019-08-05 13:27:47 -07:00
+ (BOOL)isValidPathString:(NSString *)pathString {
2017-05-15 12:27:07 -07:00
static dispatch_once_t token;
static NSCharacterSet *badPathChars = nil;
dispatch_once(&token, ^{
2019-08-05 13:27:47 -07:00
badPathChars = [NSCharacterSet
characterSetWithCharactersInString:kInvalidPathCharacters];
2017-05-15 12:27:07 -07:00
});
return pathString != nil && [pathString length] != 0 &&
2019-08-05 13:27:47 -07:00
[pathString rangeOfCharacterFromSet:badPathChars].location ==
NSNotFound;
2017-05-15 12:27:07 -07:00
}
2019-08-05 13:27:47 -07:00
+ (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];
2017-05-15 12:27:07 -07:00
}
}
2019-08-05 13:27:47 -07:00
+ (void)validateFrom:(NSString *)fn validRootPathString:(NSString *)pathString {
2017-05-15 12:27:07 -07:00
static dispatch_once_t token;
static NSRegularExpression *dotInfoRegex = nil;
dispatch_once(&token, ^{
2019-08-05 13:27:47 -07:00
dotInfoRegex = [NSRegularExpression
regularExpressionWithPattern:@"^\\/*\\.info(\\/|$)"
options:0
error:nil];
2017-05-15 12:27:07 -07:00
});
NSString *tempPath = pathString;
2019-08-05 13:27:47 -07:00
// HACK: Obj-C regex are kinda' slow. Do a plain string search first before
// bothering with the regex.
2017-05-15 12:27:07 -07:00
if ([pathString rangeOfString:@".info"].location != NSNotFound) {
2019-08-05 13:27:47 -07:00
tempPath = [dotInfoRegex
stringByReplacingMatchesInString:pathString
options:0
range:NSMakeRange(0, pathString.length)
withTemplate:@"/"];
2017-05-15 12:27:07 -07:00
}
[self validateFrom:fn validPathString:tempPath];
}
2019-08-05 13:27:47 -07:00
+ (BOOL)isValidKey:(NSString *)key {
2017-05-15 12:27:07 -07:00
static dispatch_once_t token;
static NSCharacterSet *badKeyChars = nil;
dispatch_once(&token, ^{
2019-08-05 13:27:47 -07:00
badKeyChars = [NSCharacterSet
characterSetWithCharactersInString:kInvalidKeyCharacters];
2017-05-15 12:27:07 -07:00
});
2019-08-05 13:27:47 -07:00
return key != nil && key.length > 0 &&
[key rangeOfCharacterFromSet:badKeyChars].location == NSNotFound;
2017-05-15 12:27:07 -07:00
}
2019-08-05 13:27:47 -07:00
+ (void)validateFrom:(NSString *)fn validKey:(NSString *)key {
2017-05-15 12:27:07 -07:00
if (![self isValidKey:key]) {
2019-08-05 13:27:47 -07:00
@throw [[NSException alloc]
initWithName:@"InvalidKeyValidation"
reason:[NSString
stringWithFormat:
@"(%@) Must be a non-empty string and not "
@"contain '/' '.' '#' '$' '[' or ']'",
fn]
userInfo:nil];
2017-05-15 12:27:07 -07:00
}
}
2019-08-05 13:27:47 -07:00
+ (void)validateFrom:(NSString *)fn validURL:(FParsedUrl *)parsedUrl {
NSString *pathString = [parsedUrl.path description];
2017-05-15 12:27:07 -07:00
[self validateFrom:fn validRootPathString:pathString];
}
#pragma mark -
#pragma mark Authentication validation
2019-08-05 13:27:47 -07:00
+ (BOOL)stringNonempty:(NSString *)str {
2017-05-15 12:27:07 -07:00
return str != nil && ![str isKindOfClass:[NSNull class]] && str.length > 0;
}
2019-08-05 13:27:47 -07:00
+ (void)validateToken:(NSString *)token {
2017-05-15 12:27:07 -07:00
if (![FValidation stringNonempty:token]) {
2019-08-05 13:27:47 -07:00
[NSException raise:NSInvalidArgumentException
format:@"Can't have empty string or nil for custom token"];
2017-05-15 12:27:07 -07:00
}
}
#pragma mark -
#pragma mark Handling authentication errors
/**
2019-08-05 13:27:47 -07:00
* 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 {
2017-05-15 12:27:07 -07:00
if (userCallback) {
userCallback(error, nil);
}
}
/**
2019-08-05 13:27:47 -07:00
* 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 {
2017-05-15 12:27:07 -07:00
if (userCallback) {
userCallback(error);
}
}
#pragma mark -
#pragma mark Snapshot validation
2019-08-05 13:27:47 -07:00
+ (BOOL)validateFrom:(NSString *)fn
isValidLeafValue:(id)value
withPath:(NSArray *)path {
2017-05-15 12:27:07 -07:00
if ([value isKindOfClass:[NSString class]]) {
// Try to avoid conversion to bytes if possible
2019-08-05 13:27:47 -07:00
NSString *theString = value;
if ([theString maximumLengthOfBytesUsingEncoding:NSUTF8StringEncoding] >
kFirebaseMaxLeafSize &&
[theString lengthOfBytesUsingEncoding:NSUTF8StringEncoding] >
kFirebaseMaxLeafSize) {
2017-05-15 12:27:07 -07:00
NSRange range;
range.location = 0;
range.length = MIN(path.count, 50);
2019-08-05 13:27:47 -07:00
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];
2017-05-15 12:27:07 -07:00
}
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);
2019-08-05 13:27:47 -07:00
NSString *pathString =
[[path subarrayWithRange:range] componentsJoinedByString:@"."];
@throw [[NSException alloc]
initWithName:@"InvalidFirebaseData"
reason:[NSString
stringWithFormat:
@"(%@) Cannot store NaN at path: %@.", fn,
pathString]
userInfo:nil];
2017-05-15 12:27:07 -07:00
}
return YES;
}
else if ([value isKindOfClass:[NSDictionary class]]) {
2019-08-05 13:27:47 -07:00
NSDictionary *dval = value;
2017-05-15 12:27:07 -07:00
if (dval[kServerValueSubKey] != nil) {
if ([dval count] > 1) {
NSRange range;
range.location = 0;
range.length = MIN(path.count, 50);
2019-08-05 13:27:47 -07:00
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];
2017-05-15 12:27:07 -07:00
}
return YES;
}
return NO;
}
else if (value == [NSNull null] || value == nil) {
// Null is valid type to store at leaf
return YES;
}
return NO;
}
2019-08-05 13:27:47 -07:00
+ (NSString *)parseAndValidateKey:(id)keyId
fromFunction:(NSString *)fn
path:(NSArray *)path {
2017-05-15 12:27:07 -07:00
if (![keyId isKindOfClass:[NSString class]]) {
NSRange range;
range.location = 0;
range.length = MIN(path.count, 50);
2019-08-05 13:27:47 -07:00
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];
2017-05-15 12:27:07 -07:00
}
2019-08-05 13:27:47 -07:00
return (NSString *)keyId;
2017-05-15 12:27:07 -07:00
}
2019-08-05 13:27:47 -07:00
+ (void)validateFrom:(NSString *)fn
validDictionaryKey:(id)keyId
withPath:(NSArray *)path {
2017-05-15 12:27:07 -07:00
NSString *key = [self parseAndValidateKey:keyId fromFunction:fn path:path];
2019-08-05 13:27:47 -07:00
if (![key isEqualToString:kPayloadPriority] &&
![key isEqualToString:kPayloadValue] &&
![key isEqualToString:kServerValueSubKey] &&
![FValidation isValidKey:key]) {
2017-05-15 12:27:07 -07:00
NSRange range;
range.location = 0;
range.length = MIN(path.count, 50);
2019-08-05 13:27:47 -07:00
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];
2017-05-15 12:27:07 -07:00
}
}
2019-08-05 13:27:47 -07:00
+ (void)validateFrom:(NSString *)fn
validUpdateDictionaryKey:(id)keyId
withValue:(id)value {
FPath *path = [FPath pathWithString:[self parseAndValidateKey:keyId
fromFunction:fn
path:@[]]];
2017-05-15 12:27:07 -07:00
__block NSInteger keyNum = 0;
2019-08-05 13:27:47 -07:00
[path enumerateComponentsUsingBlock:^void(NSString *key, BOOL *stop) {
if ([key isEqualToString:kPayloadPriority] &&
keyNum == [path length] - 1) {
[self validateFrom:fn isValidPriorityValue:value withPath:@[]];
} else {
2017-05-15 12:27:07 -07:00
keyNum++;
2017-06-30 10:47:37 -07:00
2017-05-15 12:27:07 -07:00
if (![FValidation isValidKey:key]) {
2019-08-05 13:27:47 -07:00
@throw [[NSException alloc]
initWithName:@"InvalidFirebaseData"
reason:[NSString
stringWithFormat:
@"(%@) Invalid key in object. Keys must "
@"be non-empty and cannot contain '.' "
@"'#' '$' '[' or ']'",
fn]
userInfo:nil];
2017-05-15 12:27:07 -07:00
}
2019-08-05 13:27:47 -07:00
}
2017-05-15 12:27:07 -07:00
}];
}
2019-08-05 13:27:47 -07:00
+ (void)validateFrom:(NSString *)fn
isValidPriorityValue:(id)value
withPath:(NSArray *)path {
[self validateFrom:fn
isValidPriorityValue:value
withPath:path
throwError:YES];
2017-05-15 12:27:07 -07:00
}
/**
2019-08-05 13:27:47 -07:00
* Returns YES if priority is valid.
*/
2017-05-15 12:27:07 -07:00
+ (BOOL)validatePriorityValue:value {
2019-08-05 13:27:47 -07:00
return [self validateFrom:nil
isValidPriorityValue:value
withPath:nil
throwError:NO];
2017-05-15 12:27:07 -07:00
}
/**
2019-08-05 13:27:47 -07:00
* 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 {
2017-05-15 12:27:07 -07:00
if ([value isKindOfClass:[NSNumber class]]) {
if ([[NSDecimalNumber notANumber] isEqualToNumber:value]) {
if (throwError) {
NSRange range;
range.location = 0;
range.length = MIN(path.count, 50);
2019-08-05 13:27:47 -07:00
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];
2017-05-15 12:27:07 -07:00
} else {
return NO;
}
2019-08-05 13:27:47 -07:00
} else if (value == (id)kCFBooleanFalse ||
value == (id)kCFBooleanTrue) {
2017-05-15 12:27:07 -07:00
if (throwError) {
NSRange range;
range.location = 0;
range.length = MIN(path.count, 50);
2019-08-05 13:27:47 -07:00
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];
2017-05-15 12:27:07 -07:00
} else {
return NO;
}
}
2019-08-05 13:27:47 -07:00
} else if ([value isKindOfClass:[NSDictionary class]]) {
2017-05-15 12:27:07 -07:00
NSDictionary *dval = value;
if (dval[kServerValueSubKey] != nil) {
if ([dval count] > 1) {
if (throwError) {
NSRange range;
range.location = 0;
range.length = MIN(path.count, 50);
2019-08-05 13:27:47 -07:00
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];
2017-05-15 12:27:07 -07:00
} else {
return NO;
}
}
} else {
if (throwError) {
NSRange range;
range.location = 0;
range.length = MIN(path.count, 50);
2019-08-05 13:27:47 -07:00
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];
2017-05-15 12:27:07 -07:00
} else {
return NO;
}
}
2019-08-05 13:27:47 -07:00
} else if ([value isKindOfClass:[NSArray class]]) {
2017-05-15 12:27:07 -07:00
if (throwError) {
NSRange range;
range.location = 0;
range.length = MIN(path.count, 50);
2019-08-05 13:27:47 -07:00
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];
2017-05-15 12:27:07 -07:00
} else {
return NO;
}
}
// It's valid!
return YES;
}
@end