diff --git a/Signal/src/ViewControllers/SelectRecipientViewController.m b/Signal/src/ViewControllers/SelectRecipientViewController.m index 9fbe046be..572f7c2ab 100644 --- a/Signal/src/ViewControllers/SelectRecipientViewController.m +++ b/Signal/src/ViewControllers/SelectRecipientViewController.m @@ -376,11 +376,17 @@ NSString *const kSelectRecipientViewControllerCellIdentifier = @"kSelectRecipien } NSString *possiblePhoneNumber = [self.callingCode stringByAppendingString:self.phoneNumberTextField.text.digitsOnly]; - PhoneNumber *parsedPhoneNumber = [PhoneNumber tryParsePhoneNumberFromUserSpecifiedText:possiblePhoneNumber]; + NSArray *parsePhoneNumbers = + [PhoneNumber tryParsePhoneNumbersFromsUserSpecifiedText:possiblePhoneNumber + clientPhoneNumber:[TSAccountManager localNumber]]; + if (parsePhoneNumbers.count < 1) { + return NO; + } + PhoneNumber *parsedPhoneNumber = parsePhoneNumbers[0]; // It'd be nice to use [PhoneNumber isValid] but it always returns false for some countries // (like afghanistan) and there doesn't seem to be a good way to determine beforehand // which countries it can validate for without forking libPhoneNumber. - return parsedPhoneNumber && parsedPhoneNumber.toE164.length > 1; + return parsedPhoneNumber.toE164.length > 1; } - (void)updatePhoneNumberButtonEnabling diff --git a/Signal/src/call/OutboundCallInitiator.swift b/Signal/src/call/OutboundCallInitiator.swift index eb8358815..3b284ff05 100644 --- a/Signal/src/call/OutboundCallInitiator.swift +++ b/Signal/src/call/OutboundCallInitiator.swift @@ -26,7 +26,7 @@ import Foundation public func initiateCall(handle: String) -> Bool { Logger.info("\(TAG) in \(#function) with handle: \(handle)") - guard let recipientId = PhoneNumber(fromUserSpecifiedText: handle)?.toE164() else { + guard let recipientId = PhoneNumber(fromE164: handle)?.toE164() else { Logger.warn("\(TAG) unable to parse signalId from phone number: \(handle)") return false } diff --git a/SignalServiceKit/src/Contacts/PhoneNumber.h b/SignalServiceKit/src/Contacts/PhoneNumber.h index 14fffd5e1..f1534b6c8 100644 --- a/SignalServiceKit/src/Contacts/PhoneNumber.h +++ b/SignalServiceKit/src/Contacts/PhoneNumber.h @@ -3,7 +3,6 @@ // #import -#import #define COUNTRY_CODE_PREFIX @"+" @@ -13,15 +12,8 @@ * Everything that expects a valid phone number should take a PhoneNumber, not a string, to avoid stringly typing. * */ -@interface PhoneNumber : NSObject { - @private - NBPhoneNumber *phoneNumber; - @private - NSString *e164; -} - -+ (PhoneNumber *)phoneNumberFromText:(NSString *)text andRegion:(NSString *)regionCode; -+ (PhoneNumber *)phoneNumberFromUserSpecifiedText:(NSString *)text; +@interface PhoneNumber : NSObject + + (PhoneNumber *)phoneNumberFromE164:(NSString *)text; + (PhoneNumber *)tryParsePhoneNumberFromText:(NSString *)text fromRegion:(NSString *)regionCode; diff --git a/SignalServiceKit/src/Contacts/PhoneNumber.m b/SignalServiceKit/src/Contacts/PhoneNumber.m index d914221d4..82c808a8a 100644 --- a/SignalServiceKit/src/Contacts/PhoneNumber.m +++ b/SignalServiceKit/src/Contacts/PhoneNumber.m @@ -2,16 +2,40 @@ // Copyright (c) 2017 Open Whisper Systems. All rights reserved. // -#import "NBAsYouTypeFormatter.h" -#import "NBPhoneNumber.h" #import "PhoneNumber.h" #import "PhoneNumberUtil.h" +#import +#import +#import +#import +#import static NSString *const RPDefaultsKeyPhoneNumberString = @"RPDefaultsKeyPhoneNumberString"; static NSString *const RPDefaultsKeyPhoneNumberCanonical = @"RPDefaultsKeyPhoneNumberCanonical"; +@interface PhoneNumber () + +@property (nonatomic, readonly) NBPhoneNumber *phoneNumber; +@property (nonatomic, readonly) NSString *e164; + +@end + +#pragma mark - + @implementation PhoneNumber +- (instancetype)initWithPhoneNumber:(NBPhoneNumber *)phoneNumber e164:(NSString *)e164 +{ + if (self = [self init]) { + OWSAssert(phoneNumber); + OWSAssert(e164.length > 0); + + _phoneNumber = phoneNumber; + _e164 = e164; + } + return self; +} + + (PhoneNumber *)phoneNumberFromText:(NSString *)text andRegion:(NSString *)regionCode { OWSAssert(text != nil); OWSAssert(regionCode != nil); @@ -32,10 +56,7 @@ static NSString *const RPDefaultsKeyPhoneNumberCanonical = @"RPDefaultsKeyPhoneN return nil; } - PhoneNumber *phoneNumber = [PhoneNumber new]; - phoneNumber->phoneNumber = number; - phoneNumber->e164 = e164; - return phoneNumber; + return [[PhoneNumber alloc] initWithPhoneNumber:number e164:e164]; } + (PhoneNumber *)phoneNumberFromUserSpecifiedText:(NSString *)text { @@ -118,8 +139,92 @@ static NSString *const RPDefaultsKeyPhoneNumberCanonical = @"RPDefaultsKeyPhoneN return [self phoneNumberFromUserSpecifiedText:sanitizedString]; } ++ (NSString *)nationalPrefixTransformRuleForDefaultRegion +{ + static NSString *result = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + NSString *defaultRegionCode = [self defaultRegionCode]; + NBMetadataHelper *helper = [[NBMetadataHelper alloc] init]; + NBPhoneMetaData *defaultRegionMetadata = [helper getMetadataForRegion:defaultRegionCode]; + result = defaultRegionMetadata.nationalPrefixTransformRule; + }); + return result; +} + +// clientPhoneNumber is the local user's phone number and should never change. ++ (NSString *)nationalPrefixTransformRuleForClientPhoneNumber:(NSString *)clientPhoneNumber +{ + if (clientPhoneNumber.length < 1) { + return nil; + } + static NSString *result = nil; + static NSString *cachedClientPhoneNumber = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + // clientPhoneNumber is the local user's phone number and should never change. + NSNumber *localCallingCode = [[PhoneNumber phoneNumberFromE164:clientPhoneNumber] getCountryCode]; + if (localCallingCode != nil) { + NSString *localCallingCodePrefix = [NSString stringWithFormat:@"+%@", localCallingCode]; + NSString *localCountryCode = + [PhoneNumberUtil.sharedUtil probableCountryCodeForCallingCode:localCallingCodePrefix]; + if (localCountryCode && ![localCountryCode isEqualToString:[self defaultRegionCode]]) { + NBMetadataHelper *helper = [[NBMetadataHelper alloc] init]; + NBPhoneMetaData *localNumberRegionMetadata = [helper getMetadataForRegion:localCountryCode]; + result = localNumberRegionMetadata.nationalPrefixTransformRule; + } + } + cachedClientPhoneNumber = [clientPhoneNumber copy]; + }); + OWSAssert([cachedClientPhoneNumber isEqualToString:clientPhoneNumber]); + return result; +} + + (NSArray *)tryParsePhoneNumbersFromsUserSpecifiedText:(NSString *)text clientPhoneNumber:(NSString *)clientPhoneNumber +{ + NSMutableArray *result = + [[self tryParsePhoneNumbersFromNormalizedText:text clientPhoneNumber:clientPhoneNumber] mutableCopy]; + + // A handful of countries (Mexico, Argentina, etc.) require a "national" prefix after + // their country calling code. + // + // It's a bit hacky, but we reconstruct these national prefixes from libPhoneNumber's + // parsing logic. It's okay if we botch this a little. The risk is that we end up with + // some misformatted numbers with extra non-numeric regex syntax. These erroneously + // parsed numbers will never be presented to the user, since they'll never survive the + // contacts intersection. + // + // 1. Try to apply a "national prefix" using the phone's region. + NSString *nationalPrefixTransformRuleForDefaultRegion = [self nationalPrefixTransformRuleForDefaultRegion]; + if ([nationalPrefixTransformRuleForDefaultRegion containsString:@"$1"]) { + NSString *normalizedText = + [nationalPrefixTransformRuleForDefaultRegion stringByReplacingOccurrencesOfString:@"$1" withString:text]; + if (![normalizedText containsString:@"$"]) { + [result addObjectsFromArray:[self tryParsePhoneNumbersFromNormalizedText:normalizedText + clientPhoneNumber:clientPhoneNumber]]; + } + } + + // 2. Try to apply a "national prefix" using the region that corresponds to the + // calling code for the local phone number. + NSString *nationalPrefixTransformRuleForClientPhoneNumber = + [self nationalPrefixTransformRuleForClientPhoneNumber:clientPhoneNumber]; + if ([nationalPrefixTransformRuleForClientPhoneNumber containsString:@"$1"]) { + NSString *normalizedText = + [nationalPrefixTransformRuleForClientPhoneNumber stringByReplacingOccurrencesOfString:@"$1" + withString:text]; + if (![normalizedText containsString:@"$"]) { + [result addObjectsFromArray:[self tryParsePhoneNumbersFromNormalizedText:normalizedText + clientPhoneNumber:clientPhoneNumber]]; + } + } + + return [result copy]; +} + ++ (NSArray *)tryParsePhoneNumbersFromNormalizedText:(NSString *)text + clientPhoneNumber:(NSString *)clientPhoneNumber { OWSAssert(text != nil); @@ -210,30 +315,32 @@ static NSString *const RPDefaultsKeyPhoneNumberCanonical = @"RPDefaultsKeyPhoneN } - (NSURL *)toSystemDialerURL { - NSString *link = [NSString stringWithFormat:@"telprompt://%@", e164]; + NSString *link = [NSString stringWithFormat:@"telprompt://%@", self.e164]; return [NSURL URLWithString:link]; } - (NSString *)toE164 { - return e164; + return self.e164; } - (NSNumber *)getCountryCode { - return phoneNumber.countryCode; + return self.phoneNumber.countryCode; } - (BOOL)isValid { - return [[PhoneNumberUtil sharedUtil].nbPhoneNumberUtil isValidNumber:phoneNumber]; + return [[PhoneNumberUtil sharedUtil].nbPhoneNumberUtil isValidNumber:self.phoneNumber]; } - (NSString *)localizedDescriptionForUser { NBPhoneNumberUtil *phoneUtil = [PhoneNumberUtil sharedUtil].nbPhoneNumberUtil; NSError *formatError = nil; - NSString *pretty = [phoneUtil format:phoneNumber numberFormat:NBEPhoneNumberFormatINTERNATIONAL error:&formatError]; + NSString *pretty = + [phoneUtil format:self.phoneNumber numberFormat:NBEPhoneNumberFormatINTERNATIONAL error:&formatError]; - if (formatError != nil) - return e164; + if (formatError != nil) { + return self.e164; + } return pretty; } @@ -242,18 +349,18 @@ static NSString *const RPDefaultsKeyPhoneNumberCanonical = @"RPDefaultsKeyPhoneN } - (NSString *)description { - return e164; + return self.e164; } - (void)encodeWithCoder:(NSCoder *)encoder { - [encoder encodeObject:phoneNumber forKey:RPDefaultsKeyPhoneNumberString]; - [encoder encodeObject:e164 forKey:RPDefaultsKeyPhoneNumberCanonical]; + [encoder encodeObject:self.phoneNumber forKey:RPDefaultsKeyPhoneNumberString]; + [encoder encodeObject:self.e164 forKey:RPDefaultsKeyPhoneNumberCanonical]; } - (id)initWithCoder:(NSCoder *)decoder { if ((self = [super init])) { - phoneNumber = [decoder decodeObjectForKey:RPDefaultsKeyPhoneNumberString]; - e164 = [decoder decodeObjectForKey:RPDefaultsKeyPhoneNumberCanonical]; + _phoneNumber = [decoder decodeObjectForKey:RPDefaultsKeyPhoneNumberString]; + _e164 = [decoder decodeObjectForKey:RPDefaultsKeyPhoneNumberCanonical]; } return self; } diff --git a/SignalServiceKit/tests/Contacts/PhoneNumberTest.m b/SignalServiceKit/tests/Contacts/PhoneNumberTest.m index 0206deb64..68ff88803 100644 --- a/SignalServiceKit/tests/Contacts/PhoneNumberTest.m +++ b/SignalServiceKit/tests/Contacts/PhoneNumberTest.m @@ -9,6 +9,8 @@ @end +#pragma mark - + @implementation PhoneNumberTest -(void)testE164 { @@ -59,4 +61,73 @@ XCTAssertEqualObjects(@"+13235551234", [actual toE164]); } +- (NSArray *)unpackTryParsePhoneNumbersFromsUserSpecifiedText:(NSString *)text + clientPhoneNumber:(NSString *)clientPhoneNumber +{ + NSArray *phoneNumbers = + [PhoneNumber tryParsePhoneNumbersFromsUserSpecifiedText:text clientPhoneNumber:clientPhoneNumber]; + NSMutableArray *result = [NSMutableArray new]; + for (PhoneNumber *phoneNumber in phoneNumbers) { + [result addObject:phoneNumber.toE164]; + } + return result; +} + +- (void)testTryParsePhoneNumbersFromsUserSpecifiedText_SimpleUSA +{ + NSArray *parsed = + [self unpackTryParsePhoneNumbersFromsUserSpecifiedText:@"323 555 1234" clientPhoneNumber:@"+13213214321"]; + XCTAssertTrue(parsed.count >= 1); + XCTAssertTrue([parsed containsObject:@"+13235551234"]); + XCTAssertEqualObjects(parsed.firstObject, @"+13235551234"); + + parsed = [self unpackTryParsePhoneNumbersFromsUserSpecifiedText:@"323-555-1234" clientPhoneNumber:@"+13213214321"]; + XCTAssertTrue(parsed.count >= 1); + XCTAssertTrue([parsed containsObject:@"+13235551234"]); + XCTAssertEqualObjects(parsed.firstObject, @"+13235551234"); + + parsed = [self unpackTryParsePhoneNumbersFromsUserSpecifiedText:@"323.555.1234" clientPhoneNumber:@"+13213214321"]; + XCTAssertTrue(parsed.count >= 1); + XCTAssertTrue([parsed containsObject:@"+13235551234"]); + XCTAssertEqualObjects(parsed.firstObject, @"+13235551234"); + + parsed = + [self unpackTryParsePhoneNumbersFromsUserSpecifiedText:@"1-323-555-1234" clientPhoneNumber:@"+13213214321"]; + XCTAssertTrue(parsed.count >= 1); + XCTAssertTrue([parsed containsObject:@"+13235551234"]); + XCTAssertEqualObjects(parsed.firstObject, @"+13235551234"); + + parsed = [self unpackTryParsePhoneNumbersFromsUserSpecifiedText:@"+13235551234" clientPhoneNumber:@"+13213214321"]; + XCTAssertTrue(parsed.count >= 1); + XCTAssertTrue([parsed containsObject:@"+13235551234"]); + XCTAssertEqualObjects(parsed.firstObject, @"+13235551234"); +} + +- (void)testTryParsePhoneNumbersFromsUserSpecifiedText_Mexico1 +{ + NSArray *parsed = + [self unpackTryParsePhoneNumbersFromsUserSpecifiedText:@"528341639157" clientPhoneNumber:@"+528341639144"]; + XCTAssertTrue(parsed.count >= 1); + XCTAssertTrue([parsed containsObject:@"+528341639157"]); + + parsed = [self unpackTryParsePhoneNumbersFromsUserSpecifiedText:@"8341639157" clientPhoneNumber:@"+528341639144"]; + XCTAssertTrue(parsed.count >= 1); + XCTAssertTrue([parsed containsObject:@"+528341639157"]); + + parsed = [self unpackTryParsePhoneNumbersFromsUserSpecifiedText:@"341639157" clientPhoneNumber:@"+528341639144"]; + XCTAssertTrue(parsed.count >= 1); + // The parsing logic should try adding Mexico's national prefix for cell numbers "1" + // after the country code. + XCTAssertTrue([parsed containsObject:@"+52341639157"]); + XCTAssertTrue([parsed containsObject:@"+521341639157"]); + + parsed = [self unpackTryParsePhoneNumbersFromsUserSpecifiedText:@"528341639157" clientPhoneNumber:@"+13213214321"]; + XCTAssertTrue(parsed.count >= 1); + XCTAssertTrue([parsed containsObject:@"+528341639157"]); + + parsed = [self unpackTryParsePhoneNumbersFromsUserSpecifiedText:@"13235551234" clientPhoneNumber:@"+528341639144"]; + XCTAssertTrue(parsed.count >= 1); + XCTAssertTrue([parsed containsObject:@"+13235551234"]); +} + @end