mirror of
https://github.com/firebase/firebase-ios-sdk
synced 2026-11-09 22:36:01 +05:45
462 lines
17 KiB
Objective-C
462 lines
17 KiB
Objective-C
/*
|
|
* 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
|