diff --git a/README.md b/README.md index 243e6e9..845e46e 100644 --- a/README.md +++ b/README.md @@ -181,3 +181,15 @@ $result = $deaDetector->isDisposable("abcdefgh@10minutemail.com"); echo $result->getDisposable()->toString()); //will print 'YES' ``` + +## Risk Detector + +The Risk-Detector checks all data in the person input, including the name, address, birthdate, +email address and phone number for fake and suspicious data. + +```php +$riskDetector = $serviceFactory->riskServices()->personRiskDetector(); +$riskResult = $riskDetector->detect($inputPerson); +var_dump($riskResult); +``` + diff --git a/src/org/nameapi/client/services/ServiceFactory.php b/src/org/nameapi/client/services/ServiceFactory.php index 10a0847..94bb881 100644 --- a/src/org/nameapi/client/services/ServiceFactory.php +++ b/src/org/nameapi/client/services/ServiceFactory.php @@ -13,6 +13,7 @@ require_once(__DIR__.'/matcher/MatcherServiceFactory.php'); require_once(__DIR__.'/formatter/FormatterServiceFactory.php'); require_once(__DIR__.'/email/EmailServiceFactory.php'); +require_once(__DIR__.'/riskdetector/RiskDetectorServiceFactory.php'); require_once(__DIR__.'/../http/RestHttpClient.php'); @@ -59,6 +60,7 @@ class ServiceFactory { private $matcherServiceFactory; private $formatterServiceFactory; private $emailServiceFactory; + private $riskServiceFactory; /** @@ -155,4 +157,15 @@ public function emailServices() { return $this->emailServiceFactory; } + /** + * @return riskdetector\RiskDetectorServiceFactory + * @since v5.3 + */ + public function riskServices() { + if ($this->riskServiceFactory==null) { + $this->riskServiceFactory = new riskdetector\RiskDetectorServiceFactory($this->apiKey, $this->context, $this->baseUrl); + } + return $this->riskServiceFactory; + } + } diff --git a/src/org/nameapi/client/services/riskdetector/DataItem.php b/src/org/nameapi/client/services/riskdetector/DataItem.php new file mode 100644 index 0000000..de7bdd1 --- /dev/null +++ b/src/org/nameapi/client/services/riskdetector/DataItem.php @@ -0,0 +1,54 @@ +value = $value; + } + + + + public function __toString() { + return $this->value; + } + +} + diff --git a/src/org/nameapi/client/services/riskdetector/DetectedRisk.php b/src/org/nameapi/client/services/riskdetector/DetectedRisk.php new file mode 100644 index 0000000..9036e6a --- /dev/null +++ b/src/org/nameapi/client/services/riskdetector/DetectedRisk.php @@ -0,0 +1,78 @@ + 1) throw new \Exception("Risk score is out of range (0,1]: ".$riskScore."!"); + $this->dataItem = $dataItem; + $this->riskType = $riskType; + $this->riskScore = $riskScore; + $this->reason = $reason; + } + + + /** + * @return DataItem + */ + public function getDataItem() { + return $this->dataItem; + } + + /** + * @return RiskType + */ + public function getRiskType() { + return $this->riskType; + } + + /** + * @return float range (0,1] the higher the worse. + */ + public function getRiskScore() { + return $this->riskScore; + } + + /** + * A one sentence text reason intended for the human that explains the risk. + */ + public function getReason() { + return $this->reason; + } + +} + diff --git a/src/org/nameapi/client/services/riskdetector/DisguiseRiskType.php b/src/org/nameapi/client/services/riskdetector/DisguiseRiskType.php new file mode 100644 index 0000000..6aba2cf --- /dev/null +++ b/src/org/nameapi/client/services/riskdetector/DisguiseRiskType.php @@ -0,0 +1,66 @@ +Such mangled input is used to circumvent machine processing.
+ * + *Humans can still understand these modified values, but machines can't unless they detect the patterns and clean + * the input.
+ * + * Possible values are are listed here. + * + * + * PADDING + * Padding is adding content to the left/right of a value. + * Example: XXXJohnXXX + * + * + * STUTTER_TYPING + * Example: Petttttttttterson + * + * + * SPACED_TYPING + * Example: P e t e r M i l l e r + * + * + * OTHER + * Everything that does not fit into any of the other categories. + * Individual categories may be created in the future. + * Currently here goes: + * - Leetspeak (using numbers instead of letters): l33t spe4k + * - Crossing fields (moving a part into the next field): ["Danie", "lJohnson"] + * This often happens unintentionally. + * - Writing out numbers where digits are expected, for example in house numbers. + * For example "twentyseven" instead of "27". + * - Using visually identical or similar letters with different Unicode values. + * Mixing scripts: For example mixing the Cyrillic with the Latin alphabet. Cyrillic has visually identical letters. + * Same script: For example using the lower case L for an upper case i (l vs I) and vice versa, using a zero 0 for an oh O. + * + */ +final class DisguiseRiskType extends RiskType { + + /** + * @var string $value + */ + private $value = null; + + public function __construct($value) { + if ($value!=='PADDING' && $value!=='STUTTER_TYPING' && $value!=='SPACED_TYPING' && $value!=='OTHER') { + throw new \Exception('Invalid value for RiskType: '.$value.'!'); + } + $this->value = $value; + } + + + + public function __toString() { + return $this->value; + } + +} + diff --git a/src/org/nameapi/client/services/riskdetector/FakeRiskType.php b/src/org/nameapi/client/services/riskdetector/FakeRiskType.php new file mode 100644 index 0000000..f4678d8 --- /dev/null +++ b/src/org/nameapi/client/services/riskdetector/FakeRiskType.php @@ -0,0 +1,94 @@ +In some situations the exact classification is difficult. + * For example a person's name may be from fiction, but also be famous at the same time. + * + * + * Possible values are are listed here. + * + * + * RANDOM_TYPING + * This kind of input is often used to quickly pass mandatory fields in a form. + * Example: "asdf asdf". + * + * + * PLACEHOLDER + * Examples: + * For person name: "John Doe". + * For person title: Example: "King Peter" + * The given name field doesn't contain a given name, but has at least a title. + * It may, in addition, contain a salutation. + * For salutation: Example: "Mr. Smith" (Mr. in the given name field). + * The given name field doesn't contain a given name, but has a salutation. + * There is no title in it, otherwise PLACEHOLDER_TITLE would be used. + * For place name: "Anytown" + * + * + * FICTIONAL + * Examples: + * For natural person: "James Bond". + * For legal person: ACME (American Company Making Everything) + * For place: "Atlantis", "Entenhausen" + * + * + * FAMOUS + * Examples: + * For natural person: "Barak Obama". + * + * + * HUMOROUS + * For natural person: "Sandy Beach". + * Place example: "Timbuckthree" + * + * + * INVALID + * This includes multiple types of invalid form input. + * Refusing input: + * Example: "None of your business" + * Placeholder nouns: "Someone", "Somebody else", "Somewhere", "Nowhere" + * Repeating the form fields: + * Example for person name: "firstname lastname" + * Examples for street: "Street" + * Vulgar language, swearing + * Examples: "fuck off" + * + * + * STRING_SIMILARITY + * The given name and surname field are equal or almost equal, or match a certain pattern. + * Example: "John" / "John" + * The risk score is culture adjusted. In some cultures such names do exist, however, a risk is still raised. + * + * + * OTHER + * Everything that does not fit into any of the other categories. + * + */ +final class FakeRiskType extends RiskType { + + /** + * @var string $value + */ + private $value = null; + + public function __construct($value) { + if ($value!=='RANDOM_TYPING' && $value!=='PLACEHOLDER' && $value!=='FICTIONAL' && $value!=='FAMOUS' && $value!=='HUMOROUS' && $value!=='INVALID' && $value!=='OTHER') { + throw new \Exception('Invalid value for RiskType: '.$value.'!'); + } + $this->value = $value; + } + + + + public function __toString() { + return $this->value; + } + +} + diff --git a/src/org/nameapi/client/services/riskdetector/RiskDetectorResult.php b/src/org/nameapi/client/services/riskdetector/RiskDetectorResult.php new file mode 100644 index 0000000..8f6231d --- /dev/null +++ b/src/org/nameapi/client/services/riskdetector/RiskDetectorResult.php @@ -0,0 +1,62 @@ + 1) throw new \Exception("Score is out of range [-1,1]: ".$score); + if ($score > 0) { + if (sizeof($risks)==0) throw new \Exception("At least one risk is required when there is a positive score!"); + } + $this->score = $score; + $this->risks = $risks; + } + + + /** + * An overall score considering all the detected risks and all the positive attributes of the record. + * + * Range [-1,0) means no risks were detected and the record looks good. + * 0 means no risks were detected, but also no positive attributes were found, the service can't tell for sure. + * Range (0,1] means one or multiple risks were detected. + * + * @return double in range [-1,1] + */ + public function getScore() { + return $this->score; + } + + public function hasRisk() { + return !empty($this->risks); + } + + /** + * Returns all the detected risks. + * @return DetectedRisk[] Sorted by severity having the worst come first. Possibly empty, guaranteed to be non-empty if the getScore() is > 0. + */ + public function getRisks() { + return $this->risks; + } + +} + diff --git a/src/org/nameapi/client/services/riskdetector/RiskDetectorServiceFactory.php b/src/org/nameapi/client/services/riskdetector/RiskDetectorServiceFactory.php new file mode 100644 index 0000000..f6c7aac --- /dev/null +++ b/src/org/nameapi/client/services/riskdetector/RiskDetectorServiceFactory.php @@ -0,0 +1,40 @@ +apiKey = $apiKey; + $this->context = $context; + $this->baseUrl = $baseUrl; + } + + /** + * @return riskdetector\PersonRiskDetectorService + */ + public function personRiskDetector() { + if ($this->personRiskDetector==null) { + $this->personRiskDetector = new riskdetector\PersonRiskDetectorService($this->apiKey, $this->context, $this->baseUrl); + } + return $this->personRiskDetector; + } + +} + diff --git a/src/org/nameapi/client/services/riskdetector/RiskType.php b/src/org/nameapi/client/services/riskdetector/RiskType.php new file mode 100644 index 0000000..4c956b3 --- /dev/null +++ b/src/org/nameapi/client/services/riskdetector/RiskType.php @@ -0,0 +1,12 @@ +riskServices()->personRiskDetector(); + * $riskResult = $riskDetector->detect($myInputPerson); + * + * @since v5.3 + */ +class PersonRiskDetectorService extends BaseService { + + private static $RESOURCE_PATH = "riskdetector/person"; + + + public function __construct($apiKey, Context $context, $baseUrl) { + parent::__construct($apiKey, $context, $baseUrl); + } + + + /** + * @param NaturalInputPerson $person + * @return RiskDetectorResult + * @throws ServiceException + */ + public function detect(NaturalInputPerson $person) { + $queryParams = array(); + $headerParams = array(); + + list($response, $httpResponseData) = $this->restHttpClient->callApiPost( + PersonRiskDetectorService::$RESOURCE_PATH, + $queryParams, $headerParams, + array('inputPerson'=>$person, 'context'=>$this->context) + ); + + try { + $score = $response->score; + $risks = array(); + foreach ($response->risks as $risk) { + $risk = new DetectedRisk( + new DataItem($risk->dataItem), + $this->_riskTypeToEnum($risk->riskType), + $risk->riskScore, + $this->_riskReason($risk) + ); + array_push($risks, $risk); + } + return new RiskDetectorResult($score, $risks); + } catch (\Exception $e) { + throw $this->unmarshallingFailed($response, $httpResponseData); + } + } + + private function _riskTypeToEnum($val) { + if ($val[0] === 'FakeRiskType') { + return new FakeRiskType($val[1]); + } else if ($val[0] === 'DisguiseRiskType') { + return new DisguiseRiskType($val[1]); + } else { + throw new \Exception("Unsupported risk class: ".$val[0]); + } + } + + private function _riskReason($val) { + if (isset($val->reason)) { + return $val->reason; + } else { + //(temporary fallback) + return "(no reason specified)"; + } + } + +} diff --git a/tests/functional/PersonRiskDetectorServiceTest.php b/tests/functional/PersonRiskDetectorServiceTest.php new file mode 100644 index 0000000..9b984f7 --- /dev/null +++ b/tests/functional/PersonRiskDetectorServiceTest.php @@ -0,0 +1,50 @@ +priority(Priority::REALTIME()) + ->build(); + $myApiKey = 'test'; //grab one from nameapi.org + $serviceFactory = new ServiceFactory($myApiKey, $context, Host::http('rc53-api.nameapi.org'), '5.0'); + $riskDetector = $serviceFactory->riskServices()->personRiskDetector(); + + //the call: + $inputPerson = NaturalInputPerson::builder() + ->name(InputPersonName::westernBuilder() + ->fullname( "John Doe" ) + ->build()) + ->gender("FEMALE") + ->build(); + $riskResult = $riskDetector->detect($inputPerson); + + //the assertions: + $this->assertTrue($riskResult->getScore() >= 0.8); + $this->assertTrue($riskResult->getScore() <= 1.0); + $this->assertTrue($riskResult->hasRisk()); + + $this->assertEquals(1, sizeof($riskResult->getRisks())); + $worstRisk = $riskResult->getRisks()[0]; + $this->assertEquals('NAME', $worstRisk->getDataItem()); + $this->assertEquals('PLACEHOLDER', $riskResult->getRisks()[0]->getRiskType()); + $this->assertTrue($riskResult->getRisks()[0]->getRiskScore() >= 0.8); + $this->assertTrue($riskResult->getRisks()[0]->getRiskScore() <= 1.0); + } + +}