Core Data淺談係列之六 : 驗證用戶輸入
在做Web開發時,需要謹記的一條原則是“絕不要相信用戶的任何輸入”(參見《Essential PHP Security》)。與網頁上的表單提交類似,做客戶端開發時也應該考慮用戶輸入,比如可以為UITextField設置代理處理用戶實時輸入的內容,也可以讀取完用戶輸入再做檢查,或者是NSManagedObject的驗證功能。
比如,我們可以在Player的實現裏提供驗證函數:
#define PLAYER_ERROR_DOMAIN @"PLAYER_ERROR_DOMAIN" enum _playerErrorCode { PLAYER_INVALID_AGE_CODE = 0, PLAYER_INVALID_NAME_CODE, PLAYER_INVALID_CODE }; typedef enum _playerErrorCode PlayerErrorCode;
@implementation Player @dynamic age; @dynamic name; @dynamic team; - (BOOL)validateName:(id *)ioValue error:(NSError **)outError { NSString *playerName = *ioValue; playerName = [playerName stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; if (!playerName || [playerName length] == 0) { if (outError) { NSString *errorStr = @"Player's name should not be empty."; NSDictionary *userInfoDict = @{ NSLocalizedDescriptionKey : errorStr }; NSError *error = [[NSError alloc] initWithDomain:PLAYER_ERROR_DOMAIN code:PLAYER_INVALID_NAME_CODE userInfo:userInfoDict]; *outError = error; } return NO; } return YES; } @end當context在執行save動作時,這些屬性驗證函數會得到調用,如果驗證函數返回NO,則save動作會失敗:

2013-01-17 22:36:42.393 cdNBA[673:c07] Error Error Domain=PLAYER_ERROR_DOMAIN Code=1 "Player's name should not be empty." UserInfo=0x827e380 {NSLocalizedDescription=Player's name should not be empty.}, Player's name should not be empty.
當然,我們絕對不會希望異常發生在這個位置,讓程序直接掛掉 —— 這裏隻是一個Demo。
因為隻有在保存context時才會調用驗證函數,為了不讓程序掛在這裏,我們可以提前進行驗證:
NSString *name = self.nameTextField.text; NSError *error = NULL; [playerObject validateValue:&name forKey:@"name" error:&error]; if (error) { NSLog(@"%@\n", [error localizedDescription]); }除了name,我們還可能驗證age或者其它屬性,同樣地可以寫相應的屬性驗證函數。但是,有些屬性的驗證是需要結合在一起作為判斷條件的。這種情況下,我們可以利用屬性間的結合驗證:
- (BOOL)validateForInsert:(NSError **)outError { BOOL valid = [super validateForInsert:outError]; NSString *playerName = self.name; if (!playerName || [playerName length] == 0) { if (outError) { NSString *errorStr = @"Player's name should not be empty."; NSDictionary *userInfoDict = @{ NSLocalizedDescriptionKey : errorStr }; NSError *error = [[NSError alloc] initWithDomain:PLAYER_ERROR_DOMAIN code:PLAYER_INVALID_NAME_CODE userInfo:userInfoDict]; *outError = [self errorFromOriginalError:error error:nil]; } valid = NO; } NSInteger playerAge = [self.age integerValue]; if (!self.age || (playerAge < 16 || playerAge > 50)) { if (outError) { NSString *errorStr = @"Player's age should be in [16, 50]."; NSDictionary *userInfoDict = @{ NSLocalizedDescriptionKey : errorStr }; NSError *error = [[NSError alloc] initWithDomain:PLAYER_ERROR_DOMAIN code:PLAYER_INVALID_AGE_CODE userInfo:userInfoDict]; *outError = [self errorFromOriginalError:*outError error:error]; } valid = NO; } return valid; } // Modified from https://developer.apple.com/library/mac/ // - (NSError *)errorFromOriginalError:(NSError *)originalError error:(NSError *)secondError { NSMutableDictionary *userInfo = [NSMutableDictionarydictionary]; NSMutableArray *errors = [NSMutableArrayarray]; if (secondError) { [errors addObject:secondError]; } if ([originalError code] == NSValidationMultipleErrorsError) { [userInfo addEntriesFromDictionary:[originalError userInfo]]; [errors addObjectsFromArray:[userInfo objectForKey:NSDetailedErrorsKey]]; } else { [errors addObject:originalError]; } [userInfo setObject:errors forKey:NSDetailedErrorsKey]; return [NSErrorerrorWithDomain:NSCocoaErrorDomain code:NSValidationMultipleErrorsError userInfo:userInfo]; }由於存在多種屬性的驗證,所以提供了錯誤信息的連接函數,這有點類似php中我們經常會寫的字符串連接點符號:
error = "Invalid username or password."; error .= "Invalid token.";
NSManagedObject提供了三個函數用戶在插入、修改、刪除之前進行驗證,分別是上麵的validateForInsert,以及validateForUpdate和validateForDelete。
這次如果name和age都為空,則會輸出如下錯誤信息:
2013-01-17 23:42:03.979 cdNBA[1064:c07] Error Error Domain=NSCocoaErrorDomain Code=1560 "The operation couldn’t be completed. (Cocoa error 1560.)" UserInfo=0x111394b0 {NSDetailedErrors=( "Error Domain=PLAYER_ERROR_DOMAIN Code=0 \"Player's age should be in [16, 50].\" UserInfo=0x1112fbf0 {NSLocalizedDescription=Player's age should be in [16, 50].}", "Error Domain=PLAYER_ERROR_DOMAIN Code=1 \"Player's name should not be empty.\" UserInfo=0x11139430 {NSLocalizedDescription=Player's name should not be empty.}" )}, The operation couldn’t be completed. (Cocoa error 1560.)
上麵隻是簡單地對name和age進行是否為空的判定,實際操作還需要判斷其它條件。比如還可以判斷該球員是否已經存在,或者是之前提到的球隊同名問題。
假設我們輸入了合法的數據,創建了一名球員的信息,結果返回到上一級視圖發現沒有得到展現。對於這種情況,我們可以先很黃很暴力地在viewWillAppear裏麵重新reload下table,或者通過觀察者模式監聽相應的消息進行刷新。這裏即將討論的方法是使用NSFetchedResultsController這個類。
Brief Talk About Core Data Series, Part 6 : Input Validation
Jason Lee @ Hangzhou
Weibo : https://weibo.com/jasonmblog
最後更新:2017-04-04 07:03:39