Merge branch '5.2' into 5.x
* 5.2: [Inflector][String] wrong plural form of words ending by "pectus" [HttpClient] Don't prepare the request in ScopingHttpClient [Console] Fixes for PHP 8.1 deprecations Make LoginRateLimiter case insentive Fix/Rewrite .gitignore regex builder Reset limiters on successful login Provide count argument for TooManyLoginAttemptsAuthenticationException to be able to translate in plural way [security] NullToken signature
This commit is contained in:
commit
252ee3975e
@ -117,7 +117,7 @@ class TextDescriptor extends Descriptor
|
|||||||
|
|
||||||
$this->writeText('<comment>Options:</comment>', $options);
|
$this->writeText('<comment>Options:</comment>', $options);
|
||||||
foreach ($definition->getOptions() as $option) {
|
foreach ($definition->getOptions() as $option) {
|
||||||
if (\strlen($option->getShortcut()) > 1) {
|
if (\strlen($option->getShortcut() ?? '') > 1) {
|
||||||
$laterOptions[] = $option;
|
$laterOptions[] = $option;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
@ -206,7 +206,7 @@ class XmlDescriptor extends Descriptor
|
|||||||
|
|
||||||
$dom->appendChild($objectXML = $dom->createElement('option'));
|
$dom->appendChild($objectXML = $dom->createElement('option'));
|
||||||
$objectXML->setAttribute('name', '--'.$option->getName());
|
$objectXML->setAttribute('name', '--'.$option->getName());
|
||||||
$pos = strpos($option->getShortcut(), '|');
|
$pos = strpos($option->getShortcut() ?? '', '|');
|
||||||
if (false !== $pos) {
|
if (false !== $pos) {
|
||||||
$objectXML->setAttribute('shortcut', '-'.substr($option->getShortcut(), 0, $pos));
|
$objectXML->setAttribute('shortcut', '-'.substr($option->getShortcut(), 0, $pos));
|
||||||
$objectXML->setAttribute('shortcuts', '-'.str_replace('|', '|-', $option->getShortcut()));
|
$objectXML->setAttribute('shortcuts', '-'.str_replace('|', '|-', $option->getShortcut()));
|
||||||
|
@ -185,7 +185,7 @@ class ProgressIndicator
|
|||||||
}
|
}
|
||||||
|
|
||||||
return $matches[0];
|
return $matches[0];
|
||||||
}, $this->format));
|
}, $this->format ?? ''));
|
||||||
}
|
}
|
||||||
|
|
||||||
private function determineBestFormat(): string
|
private function determineBestFormat(): string
|
||||||
|
@ -572,7 +572,7 @@ class Table
|
|||||||
if (isset($this->columnMaxWidths[$column]) && Helper::width(Helper::removeDecoration($formatter, $cell)) > $this->columnMaxWidths[$column]) {
|
if (isset($this->columnMaxWidths[$column]) && Helper::width(Helper::removeDecoration($formatter, $cell)) > $this->columnMaxWidths[$column]) {
|
||||||
$cell = $formatter->formatAndWrap($cell, $this->columnMaxWidths[$column] * $colspan);
|
$cell = $formatter->formatAndWrap($cell, $this->columnMaxWidths[$column] * $colspan);
|
||||||
}
|
}
|
||||||
if (!strstr($cell, "\n")) {
|
if (!strstr($cell ?? '', "\n")) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
$escaped = implode("\n", array_map([OutputFormatter::class, 'escapeTrailingBackslash'], explode("\n", $cell)));
|
$escaped = implode("\n", array_map([OutputFormatter::class, 'escapeTrailingBackslash'], explode("\n", $cell)));
|
||||||
|
@ -14,6 +14,7 @@ namespace Symfony\Component\Finder;
|
|||||||
/**
|
/**
|
||||||
* Gitignore matches against text.
|
* Gitignore matches against text.
|
||||||
*
|
*
|
||||||
|
* @author Michael Voříšek <vorismi3@fel.cvut.cz>
|
||||||
* @author Ahmed Abdou <mail@ahmd.io>
|
* @author Ahmed Abdou <mail@ahmd.io>
|
||||||
*/
|
*/
|
||||||
class Gitignore
|
class Gitignore
|
||||||
@ -21,113 +22,66 @@ class Gitignore
|
|||||||
/**
|
/**
|
||||||
* Returns a regexp which is the equivalent of the gitignore pattern.
|
* Returns a regexp which is the equivalent of the gitignore pattern.
|
||||||
*
|
*
|
||||||
* @return string The regexp
|
* Format specification: https://git-scm.com/docs/gitignore#_pattern_format
|
||||||
*/
|
*/
|
||||||
public static function toRegex(string $gitignoreFileContent): string
|
public static function toRegex(string $gitignoreFileContent): string
|
||||||
{
|
{
|
||||||
$gitignoreFileContent = preg_replace('/^[^\\\r\n]*#.*/m', '', $gitignoreFileContent);
|
$gitignoreFileContent = preg_replace('~(?<!\\\\)#[^\n\r]*~', '', $gitignoreFileContent);
|
||||||
$gitignoreLines = preg_split('/\r\n|\r|\n/', $gitignoreFileContent);
|
$gitignoreLines = preg_split('~\r\n?|\n~', $gitignoreFileContent);
|
||||||
|
|
||||||
$positives = [];
|
$res = self::lineToRegex('');
|
||||||
$negatives = [];
|
|
||||||
foreach ($gitignoreLines as $i => $line) {
|
foreach ($gitignoreLines as $i => $line) {
|
||||||
$line = trim($line);
|
$line = preg_replace('~(?<!\\\\)[ \t]+$~', '', $line);
|
||||||
if ('' === $line) {
|
|
||||||
continue;
|
if ('!' === substr($line, 0, 1)) {
|
||||||
|
$line = substr($line, 1);
|
||||||
|
$isNegative = true;
|
||||||
|
} else {
|
||||||
|
$isNegative = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (1 === preg_match('/^!/', $line)) {
|
if ('' !== $line) {
|
||||||
$positives[$i] = null;
|
if ($isNegative) {
|
||||||
$negatives[$i] = self::getRegexFromGitignore(preg_replace('/^!(.*)/', '${1}', $line), true);
|
$res = '(?!'.self::lineToRegex($line).'$)'.$res;
|
||||||
|
} else {
|
||||||
continue;
|
$res = '(?:'.$res.'|'.self::lineToRegex($line).')';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
$negatives[$i] = null;
|
|
||||||
$positives[$i] = self::getRegexFromGitignore($line);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$index = 0;
|
return '~^(?:'.$res.')~s';
|
||||||
$patterns = [];
|
|
||||||
foreach ($positives as $pattern) {
|
|
||||||
if (null === $pattern) {
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$negativesAfter = array_filter(\array_slice($negatives, ++$index));
|
private static function lineToRegex(string $gitignoreLine): string
|
||||||
if ([] !== $negativesAfter) {
|
|
||||||
$pattern .= sprintf('(?<!%s)', implode('|', $negativesAfter));
|
|
||||||
}
|
|
||||||
|
|
||||||
$patterns[] = $pattern;
|
|
||||||
}
|
|
||||||
|
|
||||||
return sprintf('/^((%s))$/', implode(')|(', $patterns));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static function getRegexFromGitignore(string $gitignorePattern, bool $negative = false): string
|
|
||||||
{
|
{
|
||||||
$regex = '';
|
if ('' === $gitignoreLine) {
|
||||||
$isRelativePath = false;
|
return '$f'; // always false
|
||||||
// If there is a separator at the beginning or middle (or both) of the pattern, then the pattern is relative to the directory level of the particular .gitignore file itself
|
|
||||||
$slashPosition = strpos($gitignorePattern, '/');
|
|
||||||
if (false !== $slashPosition && \strlen($gitignorePattern) - 1 !== $slashPosition) {
|
|
||||||
if (0 === $slashPosition) {
|
|
||||||
$gitignorePattern = substr($gitignorePattern, 1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$isRelativePath = true;
|
$slashPos = strpos($gitignoreLine, '/');
|
||||||
$regex .= '^';
|
if (false !== $slashPos && \strlen($gitignoreLine) - 1 !== $slashPos) {
|
||||||
|
if (0 === $slashPos) {
|
||||||
|
$gitignoreLine = substr($gitignoreLine, 1);
|
||||||
|
}
|
||||||
|
$isAbsolute = true;
|
||||||
|
} else {
|
||||||
|
$isAbsolute = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ('/' === $gitignorePattern[\strlen($gitignorePattern) - 1]) {
|
$parts = array_map(function (string $v): string {
|
||||||
$gitignorePattern = substr($gitignorePattern, 0, -1);
|
$v = preg_quote(str_replace('\\', '', $v), '~');
|
||||||
}
|
$v = preg_replace_callback('~\\\\\[([^\[\]]*)\\\\\]~', function (array $matches): string {
|
||||||
|
return '['.str_replace('\\-', '-', $matches[1]).']';
|
||||||
|
}, $v);
|
||||||
|
$v = preg_replace('~\\\\\*\\\\\*~', '[^/]+(?:/[^/]+)*', $v);
|
||||||
|
$v = preg_replace('~\\\\\*~', '[^/]*', $v);
|
||||||
|
$v = preg_replace('~\\\\\?~', '[^/]', $v);
|
||||||
|
|
||||||
$iMax = \strlen($gitignorePattern);
|
return $v;
|
||||||
for ($i = 0; $i < $iMax; ++$i) {
|
}, explode('/', $gitignoreLine));
|
||||||
$tripleChars = substr($gitignorePattern, $i, 3);
|
|
||||||
if ('**/' === $tripleChars || '/**' === $tripleChars) {
|
|
||||||
$regex .= '.*';
|
|
||||||
$i += 2;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$doubleChars = substr($gitignorePattern, $i, 2);
|
return ($isAbsolute ? '' : '(?:[^/]+/)*')
|
||||||
if ('**' === $doubleChars) {
|
.implode('/', $parts)
|
||||||
$regex .= '.*';
|
.('' !== end($parts) ? '(?:$|/)' : '');
|
||||||
++$i;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if ('*/' === $doubleChars) {
|
|
||||||
$regex .= '[^\/]*\/?[^\/]*';
|
|
||||||
++$i;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$c = $gitignorePattern[$i];
|
|
||||||
switch ($c) {
|
|
||||||
case '*':
|
|
||||||
$regex .= $isRelativePath ? '[^\/]*' : '[^\/]*\/?[^\/]*';
|
|
||||||
break;
|
|
||||||
case '/':
|
|
||||||
case '.':
|
|
||||||
case ':':
|
|
||||||
case '(':
|
|
||||||
case ')':
|
|
||||||
case '{':
|
|
||||||
case '}':
|
|
||||||
$regex .= '\\'.$c;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
$regex .= $c;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($negative) {
|
|
||||||
// a lookbehind assertion has to be a fixed width (it can not have nested '|' statements)
|
|
||||||
return sprintf('%s$|%s\/$', $regex, $regex);
|
|
||||||
}
|
|
||||||
|
|
||||||
return '(?>'.$regex.'($|\/.*))';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,136 +13,327 @@ namespace Symfony\Component\Finder\Tests;
|
|||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use Symfony\Component\Finder\Gitignore;
|
use Symfony\Component\Finder\Gitignore;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Michael Voříšek <vorismi3@fel.cvut.cz>
|
||||||
|
*/
|
||||||
class GitignoreTest extends TestCase
|
class GitignoreTest extends TestCase
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @dataProvider provider
|
* @dataProvider provider
|
||||||
|
* @dataProvider providerExtended
|
||||||
*/
|
*/
|
||||||
public function testCases(string $patterns, array $matchingCases, array $nonMatchingCases)
|
public function testToRegex(array $gitignoreLines, array $matchingCases, array $nonMatchingCases)
|
||||||
{
|
{
|
||||||
|
$patterns = implode("\n", $gitignoreLines);
|
||||||
|
|
||||||
$regex = Gitignore::toRegex($patterns);
|
$regex = Gitignore::toRegex($patterns);
|
||||||
|
$this->assertSame($regex, Gitignore::toRegex(implode("\r\n", $gitignoreLines)));
|
||||||
|
$this->assertSame($regex, Gitignore::toRegex(implode("\r", $gitignoreLines)));
|
||||||
|
|
||||||
foreach ($matchingCases as $matchingCase) {
|
foreach ($matchingCases as $matchingCase) {
|
||||||
$this->assertMatchesRegularExpression($regex, $matchingCase, sprintf('Failed asserting path [%s] matches gitignore patterns [%s] using regex [%s]', $matchingCase, $patterns, $regex));
|
$this->assertMatchesRegularExpression(
|
||||||
|
$regex,
|
||||||
|
$matchingCase,
|
||||||
|
sprintf(
|
||||||
|
"Failed asserting path:\n%s\nmatches gitignore patterns:\n%s",
|
||||||
|
preg_replace('~^~m', ' ', $matchingCase),
|
||||||
|
preg_replace('~^~m', ' ', $patterns)
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($nonMatchingCases as $nonMatchingCase) {
|
foreach ($nonMatchingCases as $nonMatchingCase) {
|
||||||
$this->assertDoesNotMatchRegularExpression($regex, $nonMatchingCase, sprintf('Failed asserting path [%s] not matching gitignore patterns [%s] using regex [%s]', $nonMatchingCase, $patterns, $regex));
|
$this->assertDoesNotMatchRegularExpression(
|
||||||
|
$regex,
|
||||||
|
$nonMatchingCase,
|
||||||
|
sprintf("Failed asserting path:\n%s\nNOT matching gitignore patterns:\n%s",
|
||||||
|
preg_replace('~^~m', ' ', $nonMatchingCase),
|
||||||
|
preg_replace('~^~m', ' ', $patterns)
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array return is array of
|
|
||||||
* [
|
|
||||||
* [
|
|
||||||
* '', // Git-ignore Pattern
|
|
||||||
* [], // array of file paths matching
|
|
||||||
* [], // array of file paths not matching
|
|
||||||
* ],
|
|
||||||
* ]
|
|
||||||
*/
|
|
||||||
public function provider(): array
|
public function provider(): array
|
||||||
{
|
{
|
||||||
return [
|
$cases = [
|
||||||
[
|
[
|
||||||
'
|
[''],
|
||||||
*
|
[],
|
||||||
!/bin
|
['a', 'a/b', 'a/b/c', 'aa', 'm.txt', '.txt'],
|
||||||
!/bin/bash
|
],
|
||||||
',
|
[
|
||||||
|
['a', 'X'],
|
||||||
|
['a', 'a/b', 'a/b/c', 'X', 'b/a', 'b/c/a', 'a/X', 'a/X/y', 'b/a/X/y'],
|
||||||
|
['A', 'x', 'aa', 'm.txt', '.txt', 'aa/b', 'b/aa'],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
['/a', 'x', 'd/'],
|
||||||
|
['a', 'a/b', 'a/b/c', 'x', 'a/x', 'a/x/y', 'b/a/x/y', 'd/', 'd/u', 'e/d/', 'e/d/u'],
|
||||||
|
['b/a', 'b/c/a', 'aa', 'm.txt', '.txt', 'aa/b', 'b/aa', 'e/d'],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
['a/', 'x'],
|
||||||
|
['a/b', 'a/b/c', 'x', 'a/x', 'a/x/y', 'b/a/x/y'],
|
||||||
|
['a', 'b/a', 'b/c/a', 'aa', 'm.txt', '.txt', 'aa/b', 'b/aa'],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
['*'],
|
||||||
|
['a', 'a/b', 'a/b/c', 'aa', 'm.txt', '.txt'],
|
||||||
|
[],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
['/*'],
|
||||||
|
['a', 'a/b', 'a/b/c', 'aa', 'm.txt', '.txt'],
|
||||||
|
[],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
['/a', 'm/*'],
|
||||||
|
['a', 'a/b', 'a/b/c', 'm/'],
|
||||||
|
['aa', 'm', 'b/m', 'b/m/'],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
['a', '!x'],
|
||||||
|
['a', 'a/b', 'a/b/c', 'b/a', 'b/c/a'],
|
||||||
|
['x', 'aa', 'm.txt', '.txt', 'aa/b', 'b/aa'],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
['a', '!a/', 'b', '!b/b'],
|
||||||
|
['a', 'a/x', 'x/a', 'x/a/x', 'b', 'b'],
|
||||||
|
['a/', 'x/a/', 'bb', 'b/b', 'bb'],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
['[a-c]', 'x[C-E][][o]', 'g-h'],
|
||||||
|
['a', 'b', 'c', 'xDo', 'g-h'],
|
||||||
|
['A', 'xdo', 'u', 'g', 'h'],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
['a?', '*/??b?'],
|
||||||
|
['ax', 'x/xxbx'],
|
||||||
|
['a', 'axy', 'xxax', 'x/xxax', 'x/y/xxax'],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[' ', ' \ ', ' \ ', '/a ', '/b/c \ '],
|
||||||
|
[' ', ' ', 'x/ ', 'x/ ', 'a', 'a/x', 'b/c '],
|
||||||
|
[' ', ' ', 'x/ ', 'x/ ', 'a ', 'b/c '],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
['#', ' #', '/ #', ' #', '/ #', ' \ #', ' \ #', 'a #', 'a #', 'a \ #', 'a \ #'],
|
||||||
|
[' ', ' ', 'a', 'a ', 'a '],
|
||||||
|
[' ', ' ', 'a ', 'a '],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
["\t", "\t\\\t", " \t\\\t ", "\t#", "a\t#", "a\t\t#", "a \t#", "a\t\t\\\t#", "a \t\t\\\t\t#"],
|
||||||
|
["\t\t", " \t\t", 'a', "a\t\t\t", "a \t\t\t"],
|
||||||
|
["\t", "\t\t ", " \t\t ", "a\t", 'a ', "a \t", "a\t\t"],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[' a', 'b ', '\ ', 'c\ '],
|
||||||
|
[' a', 'b', ' ', 'c '],
|
||||||
|
['a', 'b ', 'c'],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
['#a', '\#b', '\#/'],
|
||||||
|
['#b', '#/'],
|
||||||
|
['#a', 'a', 'b'],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
['*', '!!', '!!*x', '\!!b'],
|
||||||
|
['a', '!!', '!!b'],
|
||||||
|
['!', '!x', '!xx'],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
'*',
|
||||||
|
'!/bin',
|
||||||
|
'!/bin/bash',
|
||||||
|
],
|
||||||
['bin/cat', 'abc/bin/cat'],
|
['bin/cat', 'abc/bin/cat'],
|
||||||
['bin/bash'],
|
['bin/bash'],
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'fi#le.txt',
|
['fi#le.txt'],
|
||||||
[],
|
[],
|
||||||
['#file.txt'],
|
['#file.txt'],
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'
|
[
|
||||||
/bin/
|
'/bin/',
|
||||||
/usr/local/
|
'/usr/local/',
|
||||||
!/bin/bash
|
'!/bin/bash',
|
||||||
!/usr/local/bin/bash
|
'!/usr/local/bin/bash',
|
||||||
',
|
],
|
||||||
['bin/cat'],
|
['bin/cat'],
|
||||||
['bin/bash'],
|
['bin/bash'],
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'*.py[co]',
|
['*.py[co]'],
|
||||||
['file.pyc', 'file.pyc'],
|
['file.pyc', 'file.pyc'],
|
||||||
['filexpyc', 'file.pycx', 'file.py'],
|
['filexpyc', 'file.pycx', 'file.py'],
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'dir1/**/dir2/',
|
['dir1/**/dir2/'],
|
||||||
['dir1/dirA/dir2/', 'dir1/dirA/dirB/dir2/'],
|
['dir1/dirA/dir2/', 'dir1/dirA/dirB/dir2/'],
|
||||||
[],
|
[],
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'dir1/*/dir2/',
|
['dir1/*/dir2/'],
|
||||||
['dir1/dirA/dir2/'],
|
['dir1/dirA/dir2/'],
|
||||||
['dir1/dirA/dirB/dir2/'],
|
['dir1/dirA/dirB/dir2/'],
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'/*.php',
|
['/*.php'],
|
||||||
['file.php'],
|
['file.php'],
|
||||||
['app/file.php'],
|
['app/file.php'],
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'\#file.txt',
|
['\#file.txt'],
|
||||||
['#file.txt'],
|
['#file.txt'],
|
||||||
[],
|
[],
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'*.php',
|
['*.php'],
|
||||||
['app/file.php', 'file.php'],
|
['app/file.php', 'file.php'],
|
||||||
['file.phps', 'file.phps', 'filephps'],
|
['file.phps', 'file.phps', 'filephps'],
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'app/cache/',
|
['app/cache/'],
|
||||||
['app/cache/file.txt', 'app/cache/dir1/dir2/file.txt'],
|
['app/cache/file.txt', 'app/cache/dir1/dir2/file.txt'],
|
||||||
['a/app/cache/file.txt'],
|
['a/app/cache/file.txt'],
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'
|
[
|
||||||
#IamComment
|
'#IamComment',
|
||||||
/app/cache/',
|
'/app/cache/',
|
||||||
|
],
|
||||||
['app/cache/file.txt', 'app/cache/subdir/ile.txt'],
|
['app/cache/file.txt', 'app/cache/subdir/ile.txt'],
|
||||||
['a/app/cache/file.txt', '#IamComment', 'IamComment'],
|
['a/app/cache/file.txt', '#IamComment', 'IamComment'],
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'
|
[
|
||||||
/app/cache/
|
'/app/cache/',
|
||||||
#LastLineIsComment',
|
'#LastLineIsComment',
|
||||||
|
],
|
||||||
['app/cache/file.txt', 'app/cache/subdir/ile.txt'],
|
['app/cache/file.txt', 'app/cache/subdir/ile.txt'],
|
||||||
['a/app/cache/file.txt', '#LastLineIsComment', 'LastLineIsComment'],
|
['a/app/cache/file.txt', '#LastLineIsComment', 'LastLineIsComment'],
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'
|
[
|
||||||
/app/cache/
|
'/app/cache/',
|
||||||
\#file.txt
|
'\#file.txt',
|
||||||
#LastLineIsComment',
|
'#LastLineIsComment',
|
||||||
|
],
|
||||||
['app/cache/file.txt', 'app/cache/subdir/ile.txt', '#file.txt'],
|
['app/cache/file.txt', 'app/cache/subdir/ile.txt', '#file.txt'],
|
||||||
['a/app/cache/file.txt', '#LastLineIsComment', 'LastLineIsComment'],
|
['a/app/cache/file.txt', '#LastLineIsComment', 'LastLineIsComment'],
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'
|
[
|
||||||
/app/cache/
|
'/app/cache/',
|
||||||
\#file.txt
|
'\#file.txt',
|
||||||
#IamComment
|
'#IamComment',
|
||||||
another_file.txt',
|
'another_file.txt',
|
||||||
|
],
|
||||||
['app/cache/file.txt', 'app/cache/subdir/ile.txt', '#file.txt', 'another_file.txt'],
|
['app/cache/file.txt', 'app/cache/subdir/ile.txt', '#file.txt', 'another_file.txt'],
|
||||||
['a/app/cache/file.txt', 'IamComment', '#IamComment'],
|
['a/app/cache/file.txt', 'IamComment', '#IamComment'],
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'
|
[
|
||||||
/app/**
|
'/app/**',
|
||||||
!/app/bin
|
'!/app/bin',
|
||||||
!/app/bin/test
|
'!/app/bin/test',
|
||||||
',
|
],
|
||||||
['app/test/file', 'app/bin/file'],
|
['app/test/file', 'app/bin/file'],
|
||||||
['app/bin/test'],
|
['app/bin/test'],
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
'/app/*/img',
|
||||||
|
'!/app/*/img/src',
|
||||||
|
],
|
||||||
|
['app/a/img', 'app/a/img/x', 'app/a/img/src/x'],
|
||||||
|
['app/a/img/src', 'app/a/img/src/'],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
'app/**/img',
|
||||||
|
'!/app/**/img/src',
|
||||||
|
],
|
||||||
|
['app/a/img', 'app/a/img/x', 'app/a/img/src/x', 'app/a/b/img', 'app/a/b/img/x', 'app/a/b/img/src/x', 'app/a/b/c/img'],
|
||||||
|
['app/a/img/src', 'app/a/b/img/src', 'app/a/c/b/img/src'],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
'/*',
|
||||||
|
'!/foo',
|
||||||
|
'/foo/*',
|
||||||
|
'!/foo/bar',
|
||||||
|
],
|
||||||
|
['bar', 'foo/ba', 'foo/barx', 'x/foo/bar'],
|
||||||
|
['foo', 'foo/bar'],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
'/example/**',
|
||||||
|
'!/example/example.txt',
|
||||||
|
'!/example/packages',
|
||||||
|
],
|
||||||
|
['example/test', 'example/example.txt2', 'example/packages/foo.yaml'],
|
||||||
|
['example/example.txt', 'example/packages', 'example/packages/'],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
return $cases;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function providerExtended(): array
|
||||||
|
{
|
||||||
|
$basicCases = $this->provider();
|
||||||
|
|
||||||
|
$cases = [];
|
||||||
|
foreach ($basicCases as $case) {
|
||||||
|
$cases[] = [
|
||||||
|
array_merge(['never'], $case[0], ['!never']),
|
||||||
|
$case[1],
|
||||||
|
$case[2],
|
||||||
|
];
|
||||||
|
|
||||||
|
$cases[] = [
|
||||||
|
array_merge(['!*'], $case[0]),
|
||||||
|
$case[1],
|
||||||
|
$case[2],
|
||||||
|
];
|
||||||
|
|
||||||
|
$cases[] = [
|
||||||
|
array_merge(['*', '!*'], $case[0]),
|
||||||
|
$case[1],
|
||||||
|
$case[2],
|
||||||
|
];
|
||||||
|
|
||||||
|
$cases[] = [
|
||||||
|
array_merge(['never', '**/never2', 'never3/**'], $case[0]),
|
||||||
|
$case[1],
|
||||||
|
$case[2],
|
||||||
|
];
|
||||||
|
|
||||||
|
$cases[] = [
|
||||||
|
array_merge(['!never', '!**/never2', '!never3/**'], $case[0]),
|
||||||
|
$case[1],
|
||||||
|
$case[2],
|
||||||
|
];
|
||||||
|
|
||||||
|
$lines = [];
|
||||||
|
for ($i = 0; $i < 30; ++$i) {
|
||||||
|
foreach ($case[0] as $line) {
|
||||||
|
$lines[] = $line;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$cases[] = [
|
||||||
|
array_merge(['!never', '!**/never2', '!never3/**'], $lines),
|
||||||
|
$case[1],
|
||||||
|
$case[2],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return $cases;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -59,6 +59,7 @@ class ScopingHttpClient implements HttpClientInterface, ResetInterface, LoggerAw
|
|||||||
*/
|
*/
|
||||||
public function request(string $method, string $url, array $options = []): ResponseInterface
|
public function request(string $method, string $url, array $options = []): ResponseInterface
|
||||||
{
|
{
|
||||||
|
$e = null;
|
||||||
$url = self::parseUrl($url, $options['query'] ?? []);
|
$url = self::parseUrl($url, $options['query'] ?? []);
|
||||||
|
|
||||||
if (\is_string($options['base_uri'] ?? null)) {
|
if (\is_string($options['base_uri'] ?? null)) {
|
||||||
@ -72,13 +73,18 @@ class ScopingHttpClient implements HttpClientInterface, ResetInterface, LoggerAw
|
|||||||
throw $e;
|
throw $e;
|
||||||
}
|
}
|
||||||
|
|
||||||
[$url, $options] = self::prepareRequest($method, implode('', $url), $options, $this->defaultOptionsByRegexp[$this->defaultRegexp], true);
|
$options = self::mergeDefaultOptions($options, $this->defaultOptionsByRegexp[$this->defaultRegexp], true);
|
||||||
$url = implode('', $url);
|
if (\is_string($options['base_uri'] ?? null)) {
|
||||||
|
$options['base_uri'] = self::parseUrl($options['base_uri']);
|
||||||
|
}
|
||||||
|
$url = implode('', self::resolveUrl($url, $options['base_uri'] ?? null));
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($this->defaultOptionsByRegexp as $regexp => $defaultOptions) {
|
foreach ($this->defaultOptionsByRegexp as $regexp => $defaultOptions) {
|
||||||
if (preg_match("{{$regexp}}A", $url)) {
|
if (preg_match("{{$regexp}}A", $url)) {
|
||||||
|
if (null === $e || $regexp !== $this->defaultRegexp) {
|
||||||
$options = self::mergeDefaultOptions($options, $defaultOptions, true);
|
$options = self::mergeDefaultOptions($options, $defaultOptions, true);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -81,6 +81,7 @@ class InflectorTest extends TestCase
|
|||||||
['focuses', ['focus', 'focuse', 'focusis']],
|
['focuses', ['focus', 'focuse', 'focusis']],
|
||||||
['formulae', 'formula'],
|
['formulae', 'formula'],
|
||||||
['formulas', 'formula'],
|
['formulas', 'formula'],
|
||||||
|
['conspectuses', 'conspectus'],
|
||||||
['fungi', 'fungus'],
|
['fungi', 'fungus'],
|
||||||
['funguses', ['fungus', 'funguse', 'fungusis']],
|
['funguses', ['fungus', 'funguse', 'fungusis']],
|
||||||
['garages', ['garag', 'garage']],
|
['garages', ['garag', 'garage']],
|
||||||
@ -222,6 +223,7 @@ class InflectorTest extends TestCase
|
|||||||
['focus', 'focuses'],
|
['focus', 'focuses'],
|
||||||
['foot', 'feet'],
|
['foot', 'feet'],
|
||||||
['formula', 'formulas'], //formulae
|
['formula', 'formulas'], //formulae
|
||||||
|
['conspectus', 'conspectuses'],
|
||||||
['fungus', 'fungi'],
|
['fungus', 'fungi'],
|
||||||
['garage', 'garages'],
|
['garage', 'garages'],
|
||||||
['goose', 'geese'],
|
['goose', 'geese'],
|
||||||
|
@ -33,7 +33,7 @@ class NullToken implements TokenInterface
|
|||||||
|
|
||||||
public function getUser()
|
public function getUser()
|
||||||
{
|
{
|
||||||
return null;
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setUser($user)
|
public function setUser($user)
|
||||||
|
@ -33,6 +33,7 @@ class TooManyLoginAttemptsAuthenticationException extends AuthenticationExceptio
|
|||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'%minutes%' => $this->threshold,
|
'%minutes%' => $this->threshold,
|
||||||
|
'%count%' => (int) $this->threshold,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,6 +18,7 @@ use Symfony\Component\Security\Core\Exception\TooManyLoginAttemptsAuthentication
|
|||||||
use Symfony\Component\Security\Core\Security;
|
use Symfony\Component\Security\Core\Security;
|
||||||
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
|
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
|
||||||
use Symfony\Component\Security\Http\Event\CheckPassportEvent;
|
use Symfony\Component\Security\Http\Event\CheckPassportEvent;
|
||||||
|
use Symfony\Component\Security\Http\Event\LoginSuccessEvent;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Wouter de Jong <wouter@wouterj.nl>
|
* @author Wouter de Jong <wouter@wouterj.nl>
|
||||||
@ -49,10 +50,16 @@ final class LoginThrottlingListener implements EventSubscriberInterface
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function onSuccessfulLogin(LoginSuccessEvent $event): void
|
||||||
|
{
|
||||||
|
$this->limiter->reset($event->getRequest());
|
||||||
|
}
|
||||||
|
|
||||||
public static function getSubscribedEvents(): array
|
public static function getSubscribedEvents(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
CheckPassportEvent::class => ['checkPassport', 2080],
|
CheckPassportEvent::class => ['checkPassport', 2080],
|
||||||
|
LoginSuccessEvent::class => 'onSuccessfulLogin',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -39,7 +39,7 @@ final class DefaultLoginRateLimiter extends AbstractRequestRateLimiter
|
|||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
$this->globalFactory->create($request->getClientIp()),
|
$this->globalFactory->create($request->getClientIp()),
|
||||||
$this->localFactory->create($request->attributes->get(Security::LAST_USERNAME).'-'.$request->getClientIp()),
|
$this->localFactory->create(strtolower($request->attributes->get(Security::LAST_USERNAME)).'-'.$request->getClientIp()),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -63,10 +63,31 @@ class LoginThrottlingListenerTest extends TestCase
|
|||||||
$this->listener->checkPassport($this->createCheckPassportEvent($passport));
|
$this->listener->checkPassport($this->createCheckPassportEvent($passport));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$this->listener->onSuccessfulLogin($this->createLoginSuccessfulEvent($passport));
|
||||||
|
|
||||||
|
for ($i = 0; $i < 3; ++$i) {
|
||||||
|
$this->listener->checkPassport($this->createCheckPassportEvent($passport));
|
||||||
|
}
|
||||||
|
|
||||||
$this->expectException(TooManyLoginAttemptsAuthenticationException::class);
|
$this->expectException(TooManyLoginAttemptsAuthenticationException::class);
|
||||||
$this->listener->checkPassport($this->createCheckPassportEvent($passport));
|
$this->listener->checkPassport($this->createCheckPassportEvent($passport));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testPreventsLoginWithMultipleCase()
|
||||||
|
{
|
||||||
|
$request = $this->createRequest();
|
||||||
|
$passports = [$this->createPassport('wouter'), $this->createPassport('Wouter'), $this->createPassport('wOuter')];
|
||||||
|
|
||||||
|
$this->requestStack->push($request);
|
||||||
|
|
||||||
|
for ($i = 0; $i < 3; ++$i) {
|
||||||
|
$this->listener->checkPassport($this->createCheckPassportEvent($passports[$i % 3]));
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->expectException(TooManyLoginAttemptsAuthenticationException::class);
|
||||||
|
$this->listener->checkPassport($this->createCheckPassportEvent($passports[0]));
|
||||||
|
}
|
||||||
|
|
||||||
public function testPreventsLoginWhenOverGlobalThreshold()
|
public function testPreventsLoginWhenOverGlobalThreshold()
|
||||||
{
|
{
|
||||||
$request = $this->createRequest();
|
$request = $this->createRequest();
|
||||||
@ -87,12 +108,9 @@ class LoginThrottlingListenerTest extends TestCase
|
|||||||
return new SelfValidatingPassport(new UserBadge($username));
|
return new SelfValidatingPassport(new UserBadge($username));
|
||||||
}
|
}
|
||||||
|
|
||||||
private function createLoginSuccessfulEvent($passport, $username = 'wouter')
|
private function createLoginSuccessfulEvent($passport)
|
||||||
{
|
{
|
||||||
$token = $this->createMock(TokenInterface::class);
|
return new LoginSuccessEvent($this->createMock(AuthenticatorInterface::class), $passport, $this->createMock(TokenInterface::class), $this->requestStack->getCurrentRequest(), null, 'main');
|
||||||
$token->expects($this->any())->method('getUserIdentifier')->willReturn($username);
|
|
||||||
|
|
||||||
return new LoginSuccessEvent($this->createMock(AuthenticatorInterface::class), $passport, $token, $this->requestStack->getCurrentRequest(), null, 'main');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function createCheckPassportEvent($passport)
|
private function createCheckPassportEvent($passport)
|
||||||
|
@ -61,6 +61,9 @@ final class EnglishInflector implements InflectorInterface
|
|||||||
// movies (movie)
|
// movies (movie)
|
||||||
['seivom', 6, true, true, 'movie'],
|
['seivom', 6, true, true, 'movie'],
|
||||||
|
|
||||||
|
// conspectuses (conspectus), prospectuses (prospectus)
|
||||||
|
['sesutcep', 8, true, true, 'pectus'],
|
||||||
|
|
||||||
// feet (foot)
|
// feet (foot)
|
||||||
['teef', 4, true, true, 'foot'],
|
['teef', 4, true, true, 'foot'],
|
||||||
|
|
||||||
@ -267,6 +270,9 @@ final class EnglishInflector implements InflectorInterface
|
|||||||
// circuses (circus)
|
// circuses (circus)
|
||||||
['suc', 3, true, true, 'cuses'],
|
['suc', 3, true, true, 'cuses'],
|
||||||
|
|
||||||
|
// conspectuses (conspectus), prospectuses (prospectus)
|
||||||
|
['sutcep', 6, true, true, 'pectuses'],
|
||||||
|
|
||||||
// fungi (fungus), alumni (alumnus), syllabi (syllabus), radii (radius)
|
// fungi (fungus), alumni (alumnus), syllabi (syllabus), radii (radius)
|
||||||
['su', 2, true, true, 'i'],
|
['su', 2, true, true, 'i'],
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user