FValidation.m 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461
  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:
  22. // https://developer.apple.com/library/mac/#documentation/Foundation/Reference/NSRegularExpression_Class/Reference/Reference.html
  23. NSString *const kInvalidPathCharacters = @"[].#$";
  24. NSString *const kInvalidKeyCharacters = @"[].#$/";
  25. @implementation FValidation
  26. + (void)validateFrom:(NSString *)fn writablePath:(FPath *)path {
  27. if ([[path getFront] isEqualToString:kDotInfoPrefix]) {
  28. @throw [[NSException alloc]
  29. initWithName:@"WritablePathValidation"
  30. reason:[NSString
  31. stringWithFormat:@"(%@) failed to path %@: Can't "
  32. @"modify data under %@",
  33. fn, [path description],
  34. kDotInfoPrefix]
  35. userInfo:nil];
  36. }
  37. }
  38. + (void)validateFrom:(NSString *)fn knownEventType:(FIRDataEventType)event {
  39. switch (event) {
  40. case FIRDataEventTypeValue:
  41. case FIRDataEventTypeChildAdded:
  42. case FIRDataEventTypeChildChanged:
  43. case FIRDataEventTypeChildMoved:
  44. case FIRDataEventTypeChildRemoved:
  45. return;
  46. break;
  47. default:
  48. @throw [[NSException alloc]
  49. initWithName:@"KnownEventTypeValidation"
  50. reason:[NSString
  51. stringWithFormat:@"(%@) Unknown event type: %d",
  52. fn, (int)event]
  53. userInfo:nil];
  54. break;
  55. }
  56. }
  57. + (BOOL)isValidPathString:(NSString *)pathString {
  58. static dispatch_once_t token;
  59. static NSCharacterSet *badPathChars = nil;
  60. dispatch_once(&token, ^{
  61. badPathChars = [NSCharacterSet
  62. characterSetWithCharactersInString:kInvalidPathCharacters];
  63. });
  64. return pathString != nil && [pathString length] != 0 &&
  65. [pathString rangeOfCharacterFromSet:badPathChars].location ==
  66. NSNotFound;
  67. }
  68. + (void)validateFrom:(NSString *)fn validPathString:(NSString *)pathString {
  69. if (![self isValidPathString:pathString]) {
  70. @throw [[NSException alloc]
  71. initWithName:@"InvalidPathValidation"
  72. reason:[NSString stringWithFormat:
  73. @"(%@) Must be a non-empty string and "
  74. @"not contain '.' '#' '$' '[' or ']'",
  75. fn]
  76. userInfo:nil];
  77. }
  78. }
  79. + (void)validateFrom:(NSString *)fn validRootPathString:(NSString *)pathString {
  80. static dispatch_once_t token;
  81. static NSRegularExpression *dotInfoRegex = nil;
  82. dispatch_once(&token, ^{
  83. dotInfoRegex = [NSRegularExpression
  84. regularExpressionWithPattern:@"^\\/*\\.info(\\/|$)"
  85. options:0
  86. error:nil];
  87. });
  88. NSString *tempPath = pathString;
  89. // HACK: Obj-C regex are kinda' slow. Do a plain string search first before
  90. // bothering with the regex.
  91. if ([pathString rangeOfString:@".info"].location != NSNotFound) {
  92. tempPath = [dotInfoRegex
  93. stringByReplacingMatchesInString:pathString
  94. options:0
  95. range:NSMakeRange(0, pathString.length)
  96. withTemplate:@"/"];
  97. }
  98. [self validateFrom:fn validPathString:tempPath];
  99. }
  100. + (BOOL)isValidKey:(NSString *)key {
  101. static dispatch_once_t token;
  102. static NSCharacterSet *badKeyChars = nil;
  103. dispatch_once(&token, ^{
  104. badKeyChars = [NSCharacterSet
  105. characterSetWithCharactersInString:kInvalidKeyCharacters];
  106. });
  107. return key != nil && key.length > 0 &&
  108. [key rangeOfCharacterFromSet:badKeyChars].location == NSNotFound;
  109. }
  110. + (void)validateFrom:(NSString *)fn validKey:(NSString *)key {
  111. if (![self isValidKey:key]) {
  112. @throw [[NSException alloc]
  113. initWithName:@"InvalidKeyValidation"
  114. reason:[NSString
  115. stringWithFormat:
  116. @"(%@) Must be a non-empty string and not "
  117. @"contain '/' '.' '#' '$' '[' or ']'",
  118. fn]
  119. userInfo:nil];
  120. }
  121. }
  122. + (void)validateFrom:(NSString *)fn validURL:(FParsedUrl *)parsedUrl {
  123. NSString *pathString = [parsedUrl.path description];
  124. [self validateFrom:fn validRootPathString:pathString];
  125. }
  126. #pragma mark -
  127. #pragma mark Authentication validation
  128. + (BOOL)stringNonempty:(NSString *)str {
  129. return str != nil && ![str isKindOfClass:[NSNull class]] && str.length > 0;
  130. }
  131. + (void)validateToken:(NSString *)token {
  132. if (![FValidation stringNonempty:token]) {
  133. [NSException raise:NSInvalidArgumentException
  134. format:@"Can't have empty string or nil for custom token"];
  135. }
  136. }
  137. #pragma mark -
  138. #pragma mark Handling authentication errors
  139. /**
  140. * This function immediately calls the callback.
  141. * It assumes that it is not on FirebaseWorker thread.
  142. * It assumes it's on a user-controlled thread.
  143. */
  144. + (void)handleError:(NSError *)error
  145. withUserCallback:(fbt_void_nserror_id)userCallback {
  146. if (userCallback) {
  147. userCallback(error, nil);
  148. }
  149. }
  150. /**
  151. * This function immediately calls the callback.
  152. * It assumes that it is not on FirebaseWorker thread.
  153. * It assumes it's on a user-controlled thread.
  154. */
  155. + (void)handleError:(NSError *)error
  156. withSuccessCallback:(fbt_void_nserror)userCallback {
  157. if (userCallback) {
  158. userCallback(error);
  159. }
  160. }
  161. #pragma mark -
  162. #pragma mark Snapshot validation
  163. + (BOOL)validateFrom:(NSString *)fn
  164. isValidLeafValue:(id)value
  165. withPath:(NSArray *)path {
  166. if ([value isKindOfClass:[NSString class]]) {
  167. // Try to avoid conversion to bytes if possible
  168. NSString *theString = value;
  169. if ([theString maximumLengthOfBytesUsingEncoding:NSUTF8StringEncoding] >
  170. kFirebaseMaxLeafSize &&
  171. [theString lengthOfBytesUsingEncoding:NSUTF8StringEncoding] >
  172. kFirebaseMaxLeafSize) {
  173. NSRange range;
  174. range.location = 0;
  175. range.length = MIN(path.count, 50);
  176. NSString *pathString =
  177. [[path subarrayWithRange:range] componentsJoinedByString:@"."];
  178. @throw [[NSException alloc]
  179. initWithName:@"InvalidFirebaseData"
  180. reason:[NSString
  181. stringWithFormat:@"(%@) String exceeds max "
  182. @"size of %u utf8 bytes: %@",
  183. fn, (int)kFirebaseMaxLeafSize,
  184. pathString]
  185. userInfo:nil];
  186. }
  187. return YES;
  188. }
  189. else if ([value isKindOfClass:[NSNumber class]]) {
  190. // Cannot store NaN, but otherwise can store NSNumbers.
  191. if ([[NSDecimalNumber notANumber] isEqualToNumber:value]) {
  192. NSRange range;
  193. range.location = 0;
  194. range.length = MIN(path.count, 50);
  195. NSString *pathString =
  196. [[path subarrayWithRange:range] componentsJoinedByString:@"."];
  197. @throw [[NSException alloc]
  198. initWithName:@"InvalidFirebaseData"
  199. reason:[NSString
  200. stringWithFormat:
  201. @"(%@) Cannot store NaN at path: %@.", fn,
  202. pathString]
  203. userInfo:nil];
  204. }
  205. return YES;
  206. }
  207. else if ([value isKindOfClass:[NSDictionary class]]) {
  208. NSDictionary *dval = value;
  209. if (dval[kServerValueSubKey] != nil) {
  210. if ([dval count] > 1) {
  211. NSRange range;
  212. range.location = 0;
  213. range.length = MIN(path.count, 50);
  214. NSString *pathString = [[path subarrayWithRange:range]
  215. componentsJoinedByString:@"."];
  216. @throw [[NSException alloc]
  217. initWithName:@"InvalidFirebaseData"
  218. reason:[NSString stringWithFormat:
  219. @"(%@) Cannot store other keys "
  220. @"with server value keys.%@.",
  221. fn, pathString]
  222. userInfo:nil];
  223. }
  224. return YES;
  225. }
  226. return NO;
  227. }
  228. else if (value == [NSNull null] || value == nil) {
  229. // Null is valid type to store at leaf
  230. return YES;
  231. }
  232. return NO;
  233. }
  234. + (NSString *)parseAndValidateKey:(id)keyId
  235. fromFunction:(NSString *)fn
  236. path:(NSArray *)path {
  237. if (![keyId isKindOfClass:[NSString class]]) {
  238. NSRange range;
  239. range.location = 0;
  240. range.length = MIN(path.count, 50);
  241. NSString *pathString =
  242. [[path subarrayWithRange:range] componentsJoinedByString:@"."];
  243. @throw [[NSException alloc]
  244. initWithName:@"InvalidFirebaseData"
  245. reason:[NSString
  246. stringWithFormat:@"(%@) Non-string keys are not "
  247. @"allowed in object at path: %@",
  248. fn, pathString]
  249. userInfo:nil];
  250. }
  251. return (NSString *)keyId;
  252. }
  253. + (void)validateFrom:(NSString *)fn
  254. validDictionaryKey:(id)keyId
  255. withPath:(NSArray *)path {
  256. NSString *key = [self parseAndValidateKey:keyId fromFunction:fn path:path];
  257. if (![key isEqualToString:kPayloadPriority] &&
  258. ![key isEqualToString:kPayloadValue] &&
  259. ![key isEqualToString:kServerValueSubKey] &&
  260. ![FValidation isValidKey:key]) {
  261. NSRange range;
  262. range.location = 0;
  263. range.length = MIN(path.count, 50);
  264. NSString *pathString =
  265. [[path subarrayWithRange:range] componentsJoinedByString:@"."];
  266. @throw [[NSException alloc]
  267. initWithName:@"InvalidFirebaseData"
  268. reason:[NSString stringWithFormat:
  269. @"(%@) Invalid key in object at path: "
  270. @"%@. Keys must be non-empty and cannot "
  271. @"contain '/' '.' '#' '$' '[' or ']'",
  272. fn, pathString]
  273. userInfo:nil];
  274. }
  275. }
  276. + (void)validateFrom:(NSString *)fn
  277. validUpdateDictionaryKey:(id)keyId
  278. withValue:(id)value {
  279. FPath *path = [FPath pathWithString:[self parseAndValidateKey:keyId
  280. fromFunction:fn
  281. path:@[]]];
  282. __block NSInteger keyNum = 0;
  283. [path enumerateComponentsUsingBlock:^void(NSString *key, BOOL *stop) {
  284. if ([key isEqualToString:kPayloadPriority] &&
  285. keyNum == [path length] - 1) {
  286. [self validateFrom:fn isValidPriorityValue:value withPath:@[]];
  287. } else {
  288. keyNum++;
  289. if (![FValidation isValidKey:key]) {
  290. @throw [[NSException alloc]
  291. initWithName:@"InvalidFirebaseData"
  292. reason:[NSString
  293. stringWithFormat:
  294. @"(%@) Invalid key in object. Keys must "
  295. @"be non-empty and cannot contain '.' "
  296. @"'#' '$' '[' or ']'",
  297. fn]
  298. userInfo:nil];
  299. }
  300. }
  301. }];
  302. }
  303. + (void)validateFrom:(NSString *)fn
  304. isValidPriorityValue:(id)value
  305. withPath:(NSArray *)path {
  306. [self validateFrom:fn
  307. isValidPriorityValue:value
  308. withPath:path
  309. throwError:YES];
  310. }
  311. /**
  312. * Returns YES if priority is valid.
  313. */
  314. + (BOOL)validatePriorityValue:value {
  315. return [self validateFrom:nil
  316. isValidPriorityValue:value
  317. withPath:nil
  318. throwError:NO];
  319. }
  320. /**
  321. * Helper for validating priorities. If passed YES for throwError, it'll throw
  322. * descriptive errors on validation problems. Else, it'll just return YES/NO.
  323. */
  324. + (BOOL)validateFrom:(NSString *)fn
  325. isValidPriorityValue:(id)value
  326. withPath:(NSArray *)path
  327. throwError:(BOOL)throwError {
  328. if ([value isKindOfClass:[NSNumber class]]) {
  329. if ([[NSDecimalNumber notANumber] isEqualToNumber:value]) {
  330. if (throwError) {
  331. NSRange range;
  332. range.location = 0;
  333. range.length = MIN(path.count, 50);
  334. NSString *pathString = [[path subarrayWithRange:range]
  335. componentsJoinedByString:@"."];
  336. @throw [[NSException alloc]
  337. initWithName:@"InvalidFirebaseData"
  338. reason:[NSString stringWithFormat:
  339. @"(%@) Cannot store NaN as "
  340. @"priority at path: %@.",
  341. fn, pathString]
  342. userInfo:nil];
  343. } else {
  344. return NO;
  345. }
  346. } else if (value == (id)kCFBooleanFalse ||
  347. value == (id)kCFBooleanTrue) {
  348. if (throwError) {
  349. NSRange range;
  350. range.location = 0;
  351. range.length = MIN(path.count, 50);
  352. NSString *pathString = [[path subarrayWithRange:range]
  353. componentsJoinedByString:@"."];
  354. @throw [[NSException alloc]
  355. initWithName:@"InvalidFirebaseData"
  356. reason:[NSString stringWithFormat:
  357. @"(%@) Cannot store true/false "
  358. @"as priority at path: %@.",
  359. fn, pathString]
  360. userInfo:nil];
  361. } else {
  362. return NO;
  363. }
  364. }
  365. } else if ([value isKindOfClass:[NSDictionary class]]) {
  366. NSDictionary *dval = value;
  367. if (dval[kServerValueSubKey] != nil) {
  368. if ([dval count] > 1) {
  369. if (throwError) {
  370. NSRange range;
  371. range.location = 0;
  372. range.length = MIN(path.count, 50);
  373. NSString *pathString = [[path subarrayWithRange:range]
  374. componentsJoinedByString:@"."];
  375. @throw [[NSException alloc]
  376. initWithName:@"InvalidFirebaseData"
  377. reason:[NSString
  378. stringWithFormat:
  379. @"(%@) Cannot store other keys "
  380. @"with server value keys as "
  381. @"priority at path: %@.",
  382. fn, pathString]
  383. userInfo:nil];
  384. } else {
  385. return NO;
  386. }
  387. }
  388. } else {
  389. if (throwError) {
  390. NSRange range;
  391. range.location = 0;
  392. range.length = MIN(path.count, 50);
  393. NSString *pathString = [[path subarrayWithRange:range]
  394. componentsJoinedByString:@"."];
  395. @throw [[NSException alloc]
  396. initWithName:@"InvalidFirebaseData"
  397. reason:[NSString
  398. stringWithFormat:
  399. @"(%@) Cannot store an NSDictionary "
  400. @"as priority at path: %@.",
  401. fn, pathString]
  402. userInfo:nil];
  403. } else {
  404. return NO;
  405. }
  406. }
  407. } else if ([value isKindOfClass:[NSArray class]]) {
  408. if (throwError) {
  409. NSRange range;
  410. range.location = 0;
  411. range.length = MIN(path.count, 50);
  412. NSString *pathString =
  413. [[path subarrayWithRange:range] componentsJoinedByString:@"."];
  414. @throw [[NSException alloc]
  415. initWithName:@"InvalidFirebaseData"
  416. reason:[NSString stringWithFormat:
  417. @"(%@) Cannot store an NSArray as "
  418. @"priority at path: %@.",
  419. fn, pathString]
  420. userInfo:nil];
  421. } else {
  422. return NO;
  423. }
  424. }
  425. // It's valid!
  426. return YES;
  427. }
  428. @end