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.
*/
#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 ++ ;
2017-06-30 10:47:37 -07:00
2017-05-15 12:27:07 -07:00
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