diff --git a/CHANGELOG.md b/CHANGELOG.md index a566449..421e4bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,9 +10,10 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - Date formatters for formatting dates. Date formatters can be chained with word formatters. The output from the date formatter is used as input for the next formatter in the chain. - Date formatter combining the results from multiple date formatters. - Default date formatter. +- Possible to specify date formatter for guessable data rules. ### Changed -- Nothing +- Constraint weight for guessable data rules is now argument number 4 instead of number 3. Date formatter is argument number 3. ### Fixed - Nothing diff --git a/src/Rule/GuessableDataRule.php b/src/Rule/GuessableDataRule.php index 5556565..8517a13 100644 --- a/src/Rule/GuessableDataRule.php +++ b/src/Rule/GuessableDataRule.php @@ -5,6 +5,8 @@ namespace Stadly\PasswordPolice\Rule; use DateTimeInterface; +use Stadly\PasswordPolice\DateFormatter; +use Stadly\PasswordPolice\DateFormatter\DefaultFormatter; use Stadly\PasswordPolice\Password; use Stadly\PasswordPolice\Policy; use Stadly\PasswordPolice\Rule; @@ -15,47 +17,6 @@ final class GuessableDataRule implements Rule { - private const DATE_FORMATS = [ - // Year - ['Y'], // 2018 - - // Year month - ['y', 'n'], // 18 8 - ['y', 'm'], // 18 08 - ['y', 'M'], // 18 Aug - ['y', 'F'], // 18 August - - // Month year - ['n', 'y'], // 8 18 - ['M', 'y'], // Aug 18 - ['F', 'y'], // August 18 - - // Day month - ['j', 'n'], // 4 8 - ['j', 'm'], // 4 08 - ['j', 'M'], // 4 Aug - ['j', 'F'], // 4 August - - // Month day - ['n', 'j'], // 8 4 - ['n', 'd'], // 8 04 - ['M', 'j'], // Aug 4 - ['M', 'd'], // Aug 04 - ['F', 'j'], // August 4 - ['F', 'd'], // August 04 - ]; - - private const DATE_SEPARATORS = [ - '', - '-', - ' ', - '/', - '.', - ',', - '. ', - ', ', - ]; - /** * @var (string|DateTimeInterface)[] Guessable data. */ @@ -66,6 +27,11 @@ final class GuessableDataRule implements Rule */ private $wordFormatter; + /** + * @var DateFormatter Date formatter. + */ + private $dateFormatter; + /** * @var int Constraint weight. */ @@ -74,12 +40,18 @@ final class GuessableDataRule implements Rule /** * @param (string|DateTimeInterface)[] $guessableData Guessable data. * @param WordFormatter[] $wordFormatters Word formatters. + * @param DateFormatter|null $dateFormatter Date formatter. * @param int $weight Constraint weight. */ - public function __construct(array $guessableData = [], array $wordFormatters = [], int $weight = 1) - { + public function __construct( + array $guessableData = [], + array $wordFormatters = [], + ?DateFormatter $dateFormatter = null, + int $weight = 1 + ) { $this->guessableData = $guessableData; $this->wordFormatter = new FormatterCombiner($wordFormatters); + $this->dateFormatter = $dateFormatter ?? new DefaultFormatter(); $this->weight = $weight; } @@ -153,31 +125,13 @@ private function getGuessableData($password) private function contains(string $password, $data): bool { if ($data instanceof DateTimeInterface) { - return $this->containsDate($password, $data); + $strings = $this->dateFormatter->apply([$data]); + } else { + $strings = [$data]; } - return $this->containsString($password, $data); - } - - /** - * @param string $password Password to check. - * @param string $string String to check. - * @return bool Whether the password contains the string. - */ - private function containsString(string $password, string $string): bool - { - return mb_stripos($password, $string) !== false; - } - - /** - * @param string $password Password to check. - * @param DateTimeInterface $date Date to check. - * @return bool Whether the password contains the date. - */ - private function containsDate(string $password, DateTimeInterface $date): bool - { - foreach ($this->getDateFormats() as $format) { - if ($this->containsString($password, $date->format($format))) { + foreach ($strings as $string) { + if (mb_stripos($password, $string) !== false) { return true; } } @@ -185,18 +139,6 @@ private function containsDate(string $password, DateTimeInterface $date): bool return false; } - /** - * @return iterable Date formats. - */ - private function getDateFormats(): iterable - { - foreach (self::DATE_FORMATS as $format) { - foreach (self::DATE_SEPARATORS as $separator) { - yield implode($separator, $format); - } - } - } - /** * @param string|DateTimeInterface $data Data that violates the constraint. * @return string Message explaining the violation. diff --git a/tests/Rule/GuessableDataRuleTest.php b/tests/Rule/GuessableDataRuleTest.php index d373c0a..a630ded 100644 --- a/tests/Rule/GuessableDataRuleTest.php +++ b/tests/Rule/GuessableDataRuleTest.php @@ -6,6 +6,7 @@ use DateTime; use PHPUnit\Framework\TestCase; +use Stadly\PasswordPolice\DateFormatter; use Stadly\PasswordPolice\Password; use Stadly\PasswordPolice\ValidationError; use Stadly\PasswordPolice\WordFormatter; @@ -111,7 +112,7 @@ public function testPasswordCanNotContainGuessableDate(): void */ public function testRuleIsSatisfiedWhenConstraintWeightIsLowerThanTestWeight(): void { - $rule = new GuessableDataRule([], [], 1); + $rule = new GuessableDataRule([], [], null, 1); $password = new Password('foobar', ['oba']); self::assertTrue($rule->test($password, 2)); @@ -237,6 +238,26 @@ static function (iterable $words): Traversable { self::assertFalse($rule->test(new Password('apple', ['fpple']))); } + /** + * @covers ::test + */ + public function testDateFormatterCanBeCustomized(): void + { + $dateFormatter = $this->createMock(DateFormatter::class); + $dateFormatter->method('apply')->willReturnCallback( + static function (iterable $dates): Traversable { + foreach ($dates as $date) { + yield $date->format('m\o\n\t\h'); + } + } + ); + + $rule = new GuessableDataRule([], [], $dateFormatter); + + self::assertFalse($rule->test(new Password('test 11onth foo', [new DateTime('2018-11-28')]))); + self::assertTrue($rule->test(new Password('2018-11-28', [new DateTime('2018-11-28')]))); + } + /** * @covers ::validate */