FValidation.m 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312
  1. /*
  2. * Copyright 2017 Google
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. #import "FValidation.h"
  17. #import "FConstants.h"
  18. #import "FParsedUrl.h"
  19. #import "FTypedefs.h"
  20. // Have to escape: * ? + [ ( ) { } ^ $ | \ . /
  21. // See: https://developer.apple.com/library/mac/#documentation/Foundation/Reference/NSRegularExpression_Class/Reference/Reference.html
  22. NSString *const kInvalidPathCharacters = @"[].#$";
  23. NSString *const kInvalidKeyCharacters = @"[].#$/";
  24. @implementation FValidation
  25. + (void) validateFrom:(NSString *)fn writablePath:(FPath *)path {
  26. if([[path getFront] isEqualToString:kDotInfoPrefix]) {
  27. @throw [[NSException alloc] initWithName:@"WritablePathValidation" reason:[NSString stringWithFormat:@"(%@) failed to path %@: Can't modify data under %@", fn, [path description], kDotInfoPrefix] userInfo:nil];
  28. }
  29. }
  30. + (void) validateFrom:(NSString*)fn knownEventType:(FIRDataEventType)event {
  31. switch (event) {
  32. case FIRDataEventTypeValue:
  33. case FIRDataEventTypeChildAdded:
  34. case FIRDataEventTypeChildChanged:
  35. case FIRDataEventTypeChildMoved:
  36. case FIRDataEventTypeChildRemoved:
  37. return;
  38. break;
  39. default:
  40. @throw [[NSException alloc] initWithName:@"KnownEventTypeValidation" reason:[NSString stringWithFormat:@"(%@) Unknown event type: %d", fn, (int) event] userInfo:nil];
  41. break;
  42. }
  43. }
  44. + (BOOL) isValidPathString:(NSString *)pathString {
  45. static dispatch_once_t token;
  46. static NSCharacterSet *badPathChars = nil;
  47. dispatch_once(&token, ^{
  48. badPathChars = [NSCharacterSet characterSetWithCharactersInString:kInvalidPathCharacters];
  49. });
  50. return pathString != nil && [pathString length] != 0 &&
  51. [pathString rangeOfCharacterFromSet:badPathChars].location == NSNotFound;
  52. }
  53. + (void) validateFrom:(NSString *)fn validPathString:(NSString *)pathString {
  54. if(! [self isValidPathString:pathString]) {
  55. @throw [[NSException alloc] initWithName:@"InvalidPathValidation" reason:[NSString stringWithFormat:@"(%@) Must be a non-empty string and not contain '.' '#' '$' '[' or ']'", fn] userInfo:nil];
  56. }
  57. }
  58. + (void) validateFrom:(NSString *)fn validRootPathString:(NSString *)pathString {
  59. static dispatch_once_t token;
  60. static NSRegularExpression *dotInfoRegex = nil;
  61. dispatch_once(&token, ^{
  62. dotInfoRegex = [NSRegularExpression regularExpressionWithPattern:@"^\\/*\\.info(\\/|$)" options:0 error:nil];
  63. });
  64. NSString *tempPath = pathString;
  65. // HACK: Obj-C regex are kinda' slow. Do a plain string search first before bothering with the regex.
  66. if ([pathString rangeOfString:@".info"].location != NSNotFound) {
  67. tempPath = [dotInfoRegex stringByReplacingMatchesInString:pathString options:0 range:NSMakeRange(0, pathString.length) withTemplate:@"/"];
  68. }
  69. [self validateFrom:fn validPathString:tempPath];
  70. }
  71. + (BOOL) isValidKey:(NSString *)key {
  72. static dispatch_once_t token;
  73. static NSCharacterSet *badKeyChars = nil;
  74. dispatch_once(&token, ^{
  75. badKeyChars = [NSCharacterSet characterSetWithCharactersInString:kInvalidKeyCharacters];
  76. });
  77. return key != nil && key.length > 0 && [key rangeOfCharacterFromSet:badKeyChars].location == NSNotFound;
  78. }
  79. + (void) validateFrom:(NSString *)fn validKey:(NSString *)key {
  80. if (![self isValidKey:key]) {
  81. @throw [[NSException alloc] initWithName:@"InvalidKeyValidation" reason:[NSString stringWithFormat:@"(%@) Must be a non-empty string and not contain '/' '.' '#' '$' '[' or ']'", fn] userInfo:nil];
  82. }
  83. }
  84. + (void) validateFrom:(NSString *)fn validURL:(FParsedUrl *)parsedUrl {
  85. NSString* pathString = [parsedUrl.path description];
  86. [self validateFrom:fn validRootPathString:pathString];
  87. }
  88. #pragma mark -
  89. #pragma mark Authentication validation
  90. + (BOOL) stringNonempty:(NSString *)str {
  91. return str != nil && ![str isKindOfClass:[NSNull class]] && str.length > 0;
  92. }
  93. + (void) validateToken:(NSString *)token {
  94. if (![FValidation stringNonempty:token]) {
  95. [NSException raise:NSInvalidArgumentException format:@"Can't have empty string or nil for custom token"];
  96. }
  97. }
  98. #pragma mark -
  99. #pragma mark Handling authentication errors
  100. /**
  101. * This function immediately calls the callback.
  102. * It assumes that it is not on FirebaseWorker thread.
  103. * It assumes it's on a user-controlled thread.
  104. */
  105. + (void) handleError:(NSError *)error withUserCallback:(fbt_void_nserror_id)userCallback {
  106. if (userCallback) {
  107. userCallback(error, nil);
  108. }
  109. }
  110. /**
  111. * This function immediately calls the callback.
  112. * It assumes that it is not on FirebaseWorker thread.
  113. * It assumes it's on a user-controlled thread.
  114. */
  115. + (void) handleError:(NSError *)error withSuccessCallback:(fbt_void_nserror)userCallback {
  116. if (userCallback) {
  117. userCallback(error);
  118. }
  119. }
  120. #pragma mark -
  121. #pragma mark Snapshot validation
  122. + (BOOL) validateFrom:(NSString*)fn isValidLeafValue:(id)value withPath:(NSArray*)path {
  123. if ([value isKindOfClass:[NSString class]]) {
  124. // Try to avoid conversion to bytes if possible
  125. NSString* theString = value;
  126. if ([theString maximumLengthOfBytesUsingEncoding:NSUTF8StringEncoding] > kFirebaseMaxLeafSize &&
  127. [theString lengthOfBytesUsingEncoding:NSUTF8StringEncoding] > kFirebaseMaxLeafSize) {
  128. NSRange range;
  129. range.location = 0;
  130. range.length = MIN(path.count, 50);
  131. NSString* pathString = [[path subarrayWithRange:range] componentsJoinedByString:@"."];
  132. @throw [[NSException alloc] initWithName:@"InvalidFirebaseData" reason:[NSString stringWithFormat:@"(%@) String exceeds max size of %u utf8 bytes: %@", fn, (int)kFirebaseMaxLeafSize, pathString] userInfo:nil];
  133. }
  134. return YES;
  135. }
  136. else if ([value isKindOfClass:[NSNumber class]]) {
  137. // Cannot store NaN, but otherwise can store NSNumbers.
  138. if ([[NSDecimalNumber notANumber] isEqualToNumber:value]) {
  139. NSRange range;
  140. range.location = 0;
  141. range.length = MIN(path.count, 50);
  142. NSString* pathString = [[path subarrayWithRange:range] componentsJoinedByString:@"."];
  143. @throw [[NSException alloc] initWithName:@"InvalidFirebaseData" reason:[NSString stringWithFormat:@"(%@) Cannot store NaN at path: %@.", fn, pathString] userInfo:nil];
  144. }
  145. return YES;
  146. }
  147. else if ([value isKindOfClass:[NSDictionary class]]) {
  148. NSDictionary* dval = value;
  149. if (dval[kServerValueSubKey] != nil) {
  150. if ([dval count] > 1) {
  151. NSRange range;
  152. range.location = 0;
  153. range.length = MIN(path.count, 50);
  154. NSString* pathString = [[path subarrayWithRange:range] componentsJoinedByString:@"."];
  155. @throw [[NSException alloc] initWithName:@"InvalidFirebaseData" reason:[NSString stringWithFormat:@"(%@) Cannot store other keys with server value keys.%@.", fn, pathString] userInfo:nil];
  156. }
  157. return YES;
  158. }
  159. return NO;
  160. }
  161. else if (value == [NSNull null] || value == nil) {
  162. // Null is valid type to store at leaf
  163. return YES;
  164. }
  165. return NO;
  166. }
  167. + (NSString*) parseAndValidateKey:(id)keyId fromFunction:(NSString*)fn path:(NSArray*)path {
  168. if (![keyId isKindOfClass:[NSString class]]) {
  169. NSRange range;
  170. range.location = 0;
  171. range.length = MIN(path.count, 50);
  172. NSString* pathString = [[path subarrayWithRange:range] componentsJoinedByString:@"."];
  173. @throw [[NSException alloc] initWithName:@"InvalidFirebaseData" reason:[NSString stringWithFormat:@"(%@) Non-string keys are not allowed in object at path: %@", fn, pathString] userInfo:nil];
  174. }
  175. return (NSString*)keyId;
  176. }
  177. + (void) validateFrom:(NSString*)fn validDictionaryKey:(id)keyId withPath:(NSArray*)path {
  178. NSString *key = [self parseAndValidateKey:keyId fromFunction:fn path:path];
  179. if (![key isEqualToString:kPayloadPriority] && ![key isEqualToString:kPayloadValue] && ![key isEqualToString:kServerValueSubKey] && ![FValidation isValidKey:key]) {
  180. NSRange range;
  181. range.location = 0;
  182. range.length = MIN(path.count, 50);
  183. NSString *pathString = [[path subarrayWithRange:range] componentsJoinedByString:@"."];
  184. @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];
  185. }
  186. }
  187. + (void) validateFrom:(NSString*)fn validUpdateDictionaryKey:(id)keyId withValue:(id)value {
  188. FPath *path = [FPath pathWithString:[self parseAndValidateKey:keyId fromFunction:fn path:@[]]];
  189. __block NSInteger keyNum = 0;
  190. [path enumerateComponentsUsingBlock:^void (NSString *key, BOOL *stop) {
  191. if ([key isEqualToString:kPayloadPriority] && keyNum == [path length] - 1) {
  192. [self validateFrom:fn isValidPriorityValue:value withPath:@[]];
  193. } else {
  194. keyNum++;
  195. if (![FValidation isValidKey:key]) {
  196. @throw [[NSException alloc] initWithName:@"InvalidFirebaseData" reason:[NSString stringWithFormat:@"(%@) Invalid key in object. Keys must be non-empty and cannot contain '.' '#' '$' '[' or ']'", fn] userInfo:nil];
  197. }
  198. }
  199. }];
  200. }
  201. + (void) validateFrom:(NSString*)fn isValidPriorityValue:(id)value withPath:(NSArray*)path {
  202. [self validateFrom:fn isValidPriorityValue:value withPath:path throwError:YES];
  203. }
  204. /**
  205. * Returns YES if priority is valid.
  206. */
  207. + (BOOL)validatePriorityValue:value {
  208. return [self validateFrom:nil isValidPriorityValue:value withPath:nil throwError:NO];
  209. }
  210. /**
  211. * Helper for validating priorities. If passed YES for throwError, it'll throw descriptive errors on validation
  212. * problems. Else, it'll just return YES/NO.
  213. */
  214. + (BOOL) validateFrom:(NSString*)fn isValidPriorityValue:(id)value withPath:(NSArray*)path throwError:(BOOL)throwError {
  215. if ([value isKindOfClass:[NSNumber class]]) {
  216. if ([[NSDecimalNumber notANumber] isEqualToNumber:value]) {
  217. if (throwError) {
  218. NSRange range;
  219. range.location = 0;
  220. range.length = MIN(path.count, 50);
  221. NSString *pathString = [[path subarrayWithRange:range] componentsJoinedByString:@"."];
  222. @throw [[NSException alloc] initWithName:@"InvalidFirebaseData" reason:[NSString stringWithFormat:@"(%@) Cannot store NaN as priority at path: %@.", fn, pathString] userInfo:nil];
  223. } else {
  224. return NO;
  225. }
  226. } else if (value == (id) kCFBooleanFalse || value == (id) kCFBooleanTrue) {
  227. if (throwError) {
  228. NSRange range;
  229. range.location = 0;
  230. range.length = MIN(path.count, 50);
  231. NSString *pathString = [[path subarrayWithRange:range] componentsJoinedByString:@"."];
  232. @throw [[NSException alloc] initWithName:@"InvalidFirebaseData" reason:[NSString stringWithFormat:@"(%@) Cannot store true/false as priority at path: %@.", fn, pathString] userInfo:nil];
  233. } else {
  234. return NO;
  235. }
  236. }
  237. }
  238. else if ([value isKindOfClass:[NSDictionary class]]) {
  239. NSDictionary *dval = value;
  240. if (dval[kServerValueSubKey] != nil) {
  241. if ([dval count] > 1) {
  242. if (throwError) {
  243. NSRange range;
  244. range.location = 0;
  245. range.length = MIN(path.count, 50);
  246. NSString *pathString = [[path subarrayWithRange:range] componentsJoinedByString:@"."];
  247. @throw [[NSException alloc] initWithName:@"InvalidFirebaseData" reason:[NSString stringWithFormat:@"(%@) Cannot store other keys with server value keys as priority at path: %@.", fn, pathString] userInfo:nil];
  248. } else {
  249. return NO;
  250. }
  251. }
  252. } else {
  253. if (throwError) {
  254. NSRange range;
  255. range.location = 0;
  256. range.length = MIN(path.count, 50);
  257. NSString *pathString = [[path subarrayWithRange:range] componentsJoinedByString:@"."];
  258. @throw [[NSException alloc] initWithName:@"InvalidFirebaseData" reason:[NSString stringWithFormat:@"(%@) Cannot store an NSDictionary as priority at path: %@.", fn, pathString] userInfo:nil];
  259. } else {
  260. return NO;
  261. }
  262. }
  263. }
  264. else if ([value isKindOfClass:[NSArray class]]) {
  265. if (throwError) {
  266. NSRange range;
  267. range.location = 0;
  268. range.length = MIN(path.count, 50);
  269. NSString *pathString = [[path subarrayWithRange:range] componentsJoinedByString:@"."];
  270. @throw [[NSException alloc] initWithName:@"InvalidFirebaseData" reason:[NSString stringWithFormat:@"(%@) Cannot store an NSArray as priority at path: %@.", fn, pathString] userInfo:nil];
  271. } else {
  272. return NO;
  273. }
  274. }
  275. // It's valid!
  276. return YES;
  277. }
  278. @end