459
技術社區[雲棲]
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