Skip to content

Commit dde9679

Browse files
committed
Merge branch 'AlexLisenkov-add-custom-matcher'
2 parents 7153931 + f2f91f3 commit dde9679

File tree

9 files changed

+123
-41
lines changed

9 files changed

+123
-41
lines changed

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
"symfony/polyfill-mbstring": ">=1.3.1"
1717
},
1818
"require-dev": {
19-
"phpunit/phpunit": "^6.0",
19+
"phpunit/phpunit": "^7.0",
2020
"php-coveralls/php-coveralls": "*",
2121
"squizlabs/php_codesniffer": "3.*"
2222
},

phpunit.xml.dist

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
convertWarningsToExceptions="true"
77
processIsolation="false"
88
stopOnFailure="false"
9-
syntaxCheck="false"
109
bootstrap="test/config/bootstrap.php">
1110

1211
<testsuites>
@@ -22,9 +21,9 @@
2221
</filter>
2322

2423
<logging>
25-
<log type="coverage-html" target="build/coverage" charset="UTF-8"/>
26-
<log type="coverage-clover" target="build/logs/clover.xml" charset="UTF-8"/>
27-
<log type="junit" target="build/logs/junit.xml" logIncompleteSkipped="false"/>
24+
<log type="coverage-html" target="build/coverage"/>
25+
<log type="coverage-clover" target="build/logs/clover.xml"/>
26+
<log type="junit" target="build/logs/junit.xml"/>
2827
</logging>
2928

3029
</phpunit>

src/Feedback.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ class Feedback
2020
public function getFeedback($score, array $sequence)
2121
{
2222
// starting feedback
23-
if (count($sequence) == 0) {
23+
if (count($sequence) === 0) {
2424
return [
2525
'warning' => '',
2626
'suggestions' => [
@@ -46,7 +46,7 @@ public function getFeedback($score, array $sequence)
4646
}
4747
}
4848

49-
$feedback = $longestMatch->getFeedback(count($sequence) == 1);
49+
$feedback = $longestMatch->getFeedback(count($sequence) === 1);
5050
$extraFeedback = 'Add another word or two. Uncommon words are better.';
5151

5252
array_unshift($feedback['suggestions'], $extraFeedback);

src/Matcher.php

Lines changed: 34 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,23 @@
33
namespace ZxcvbnPhp;
44

55
use ZxcvbnPhp\Matchers\Match;
6+
use ZxcvbnPhp\Matchers\MatchInterface;
67

78
class Matcher
89
{
10+
private const DEFAULT_MATCHERS = [
11+
Matchers\DateMatch::class,
12+
Matchers\DictionaryMatch::class,
13+
Matchers\ReverseDictionaryMatch::class,
14+
Matchers\L33tMatch::class,
15+
Matchers\RepeatMatch::class,
16+
Matchers\SequenceMatch::class,
17+
Matchers\SpatialMatch::class,
18+
Matchers\YearMatch::class,
19+
];
20+
21+
private $additionalMatchers = [];
22+
923
/**
1024
* Get matches for a password.
1125
*
@@ -24,14 +38,27 @@ public function getMatches($password, array $userInputs = [])
2438
foreach ($this->getMatchers() as $matcher) {
2539
$matched = $matcher::match($password, $userInputs);
2640
if (is_array($matched) && !empty($matched)) {
27-
$matches = array_merge($matches, $matched);
41+
$matches[] = $matched;
2842
}
2943
}
3044

45+
$matches = array_merge([], ...$matches);
3146
self::usortStable($matches, [$this, 'compareMatches']);
47+
3248
return $matches;
3349
}
3450

51+
public function addMatcher(string $className)
52+
{
53+
if (!is_a($className, MatchInterface::class, true)) {
54+
throw new \InvalidArgumentException(sprintf('Matcher class must implement %s', MatchInterface::class));
55+
}
56+
57+
$this->additionalMatchers[$className] = $className;
58+
59+
return $this;
60+
}
61+
3562
/**
3663
* A stable implementation of usort().
3764
*
@@ -46,14 +73,14 @@ public function getMatches($password, array $userInputs = [])
4673
* @param callable $value_compare_func
4774
* @return bool
4875
*/
49-
public static function usortStable(array &$array, $value_compare_func)
76+
public static function usortStable(array &$array, callable $value_compare_func)
5077
{
5178
$index = 0;
5279
foreach ($array as &$item) {
5380
$item = array($index++, $item);
5481
}
5582
$result = usort($array, function ($a, $b) use ($value_compare_func) {
56-
$result = call_user_func($value_compare_func, $a[1], $b[1]);
83+
$result = $value_compare_func($a[1], $b[1]);
5784
return $result == 0 ? $a[0] - $b[0] : $result;
5885
});
5986
foreach ($array as &$item) {
@@ -78,16 +105,9 @@ public static function compareMatches(Match $a, Match $b)
78105
*/
79106
protected function getMatchers()
80107
{
81-
// @todo change to dynamic
82-
return [
83-
Matchers\DateMatch::class,
84-
Matchers\DictionaryMatch::class,
85-
Matchers\ReverseDictionaryMatch::class,
86-
Matchers\L33tMatch::class,
87-
Matchers\RepeatMatch::class,
88-
Matchers\SequenceMatch::class,
89-
Matchers\SpatialMatch::class,
90-
Matchers\YearMatch::class,
91-
];
108+
return array_merge(
109+
self::DEFAULT_MATCHERS,
110+
array_values($this->additionalMatchers)
111+
);
92112
}
93113
}

src/Matchers/DateMatch.php

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ class DateMatch extends Match
1414
public const MAX_YEAR = 2050;
1515

1616
public const MIN_YEAR_SPACE = 20;
17-
17+
1818
public $pattern = 'date';
1919

2020
private static $DATE_SPLITS = [
@@ -369,13 +369,15 @@ protected static function twoToFourDigitYear($year)
369369
{
370370
if ($year > 99) {
371371
return $year;
372-
} elseif ($year > 50) {
372+
}
373+
374+
if ($year > 50) {
373375
// 87 -> 1987
374376
return $year + 1900;
375-
} else {
376-
// 15 -> 2015
377-
return $year + 2000;
378377
}
378+
379+
// 15 -> 2015
380+
return $year + 2000;
379381
}
380382

381383
/**

src/TimeEstimator.php

Lines changed: 33 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -38,20 +38,26 @@ protected function guessesToScore($guesses)
3838
if ($guesses < 1e3 + $DELTA) {
3939
# risky password: "too guessable"
4040
return 0;
41-
} elseif ($guesses < 1e6 + $DELTA) {
41+
}
42+
43+
if ($guesses < 1e6 + $DELTA) {
4244
# modest protection from throttled online attacks: "very guessable"
4345
return 1;
44-
} elseif ($guesses < 1e8 + $DELTA) {
46+
}
47+
48+
if ($guesses < 1e8 + $DELTA) {
4549
# modest protection from unthrottled online attacks: "somewhat guessable"
4650
return 2;
47-
} elseif ($guesses < 1e10 + $DELTA) {
51+
}
52+
53+
if ($guesses < 1e10 + $DELTA) {
4854
# modest protection from offline attacks: "safely unguessable"
4955
# assuming a salted, slow hash function like bcrypt, scrypt, PBKDF2, argon, etc
5056
return 3;
51-
} else {
52-
# strong protection from offline attacks under same scenario: "very unguessable"
53-
return 4;
5457
}
58+
59+
# strong protection from offline attacks under same scenario: "very unguessable"
60+
return 4;
5561
}
5662

5763
protected function displayTime($seconds)
@@ -66,33 +72,45 @@ protected function displayTime($seconds)
6672

6773
if ($seconds < 1) {
6874
return [null, 'less than a second'];
69-
} elseif ($seconds < $minute) {
75+
}
76+
77+
if ($seconds < $minute) {
7078
$base = round($seconds);
7179
return [$base, "$base second"];
72-
} elseif ($seconds < $hour) {
80+
}
81+
82+
if ($seconds < $hour) {
7383
$base = round($seconds / $minute);
7484
return [$base, "$base minute"];
75-
} elseif ($seconds < $day) {
85+
}
86+
87+
if ($seconds < $day) {
7688
$base = round($seconds / $hour);
7789
return [$base, "$base hour"];
78-
} elseif ($seconds < $month) {
90+
}
91+
92+
if ($seconds < $month) {
7993
$base = round($seconds / $day);
8094
return [$base, "$base day"];
81-
} elseif ($seconds < $year) {
95+
}
96+
97+
if ($seconds < $year) {
8298
$base = round($seconds / $month);
8399
return [$base, "$base month"];
84-
} elseif ($seconds < $century) {
100+
}
101+
102+
if ($seconds < $century) {
85103
$base = round($seconds / $year);
86104
return [$base, "$base year"];
87-
} else {
88-
return [null, 'centuries'];
89105
}
106+
107+
return [null, 'centuries'];
90108
};
91109

92110
list($display_num, $display_str) = $callback($seconds);
93111

94112
if ($display_num > 1) {
95-
$display_str .= "s";
113+
$display_str .= 's';
96114
}
97115

98116
return $display_str;

src/Zxcvbn.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,13 @@ public function __construct()
3737
$this->feedback = new \ZxcvbnPhp\Feedback();
3838
}
3939

40+
public function addMatcher(string $className)
41+
{
42+
$this->matcher->addMatcher($className);
43+
44+
return $this;
45+
}
46+
4047
/**
4148
* Calculate password strength via non-overlapping minimum entropy patterns.
4249
*

test/MatcherTest.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use PHPUnit\Framework\TestCase;
66
use ZxcvbnPhp\Matcher;
7+
use ZxcvbnPhp\Matchers\Bruteforce;
78
use ZxcvbnPhp\Matchers\DictionaryMatch;
89

910
/**
@@ -63,4 +64,22 @@ public function testUserDefinedWords()
6364
$this->assertInstanceOf(DictionaryMatch::class, $matches[0], "user input match is correct class");
6465
$this->assertEquals('wQbg', $matches[0]->token, "user input match has correct token");
6566
}
67+
68+
public function testAddMatcherWillThrowException()
69+
{
70+
$this->expectException(\InvalidArgumentException::class);
71+
72+
$matcher = new Matcher();
73+
$matcher->addMatcher('invalid className');
74+
75+
$this->expectNotToPerformAssertions();
76+
}
77+
78+
public function testAddMatcherWillReturnSelf()
79+
{
80+
$matcher = new Matcher();
81+
$result = $matcher->addMatcher(Bruteforce::class);
82+
83+
$this->assertSame($matcher, $result);
84+
}
6685
}

test/ZxcvbnTest.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace ZxcvbnPhp\Test;
44

55
use PHPUnit\Framework\TestCase;
6+
use ZxcvbnPhp\Matchers\Bruteforce;
67
use ZxcvbnPhp\Matchers\DictionaryMatch;
78
use ZxcvbnPhp\Matchers\Match;
89
use ZxcvbnPhp\Zxcvbn;
@@ -130,4 +131,20 @@ public function testMultibyteUserDefinedWords()
130131
$this->assertInstanceOf(DictionaryMatch::class, $result['sequence'][0], "user input match is correct class");
131132
$this->assertEquals('المفاتيح', $result['sequence'][0]->token, "user input match has correct token");
132133
}
134+
135+
public function testAddMatcherWillThrowException()
136+
{
137+
$this->expectException(\InvalidArgumentException::class);
138+
139+
$this->zxcvbn->addMatcher('invalid className');
140+
141+
$this->expectNotToPerformAssertions();
142+
}
143+
144+
public function testAddMatcherWillReturnSelf()
145+
{
146+
$result = $this->zxcvbn->addMatcher(Bruteforce::class);
147+
148+
$this->assertSame($this->zxcvbn, $result);
149+
}
133150
}

0 commit comments

Comments
 (0)