Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

[HttpFoundation] Add ParameterBag::getString() and deprecate accepting invalid values #48525

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions 6 UPGRADE-6.3.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,12 @@ HttpFoundation

* `Response::sendHeaders()` now takes an optional `$statusCode` parameter

HttpFoundation
--------------

* Deprecate conversion of invalid values in `ParameterBag::getInt()` and `ParameterBag::getBoolean()`
* Deprecate ignoring invalid values when using `ParameterBag::filter()`, unless flag `FILTER_NULL_ON_FAILURE` is set

nicolas-grekas marked this conversation as resolved.
Show resolved Hide resolved
HttpKernel
----------

Expand Down
4 changes: 4 additions & 0 deletions 4 src/Symfony/Component/HttpFoundation/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,14 @@ CHANGELOG
6.3
GromNaN marked this conversation as resolved.
Show resolved Hide resolved
---
GromNaN marked this conversation as resolved.
Show resolved Hide resolved

* Calling `ParameterBag::getDigit()`, `getAlnum()`, `getAlpha()` on an `array` throws a `UnexpectedValueException` instead of a `TypeError`
* Add `ParameterBag::getString()` to convert a parameter into string and throw an exception if the value is invalid
* Add `ParameterBag::getEnum()`
* Create migration for session table when pdo handler is used
* Add support for Relay PHP extension for Redis
* The `Response::sendHeaders()` method now takes an optional HTTP status code as parameter, allowing to send informational responses such as Early Hints responses (103 status code)
* Deprecate conversion of invalid values in `ParameterBag::getInt()` and `ParameterBag::getBoolean()`,
* Deprecate ignoring invalid values when using `ParameterBag::filter()`, unless flag `FILTER_NULL_ON_FAILURE` is set

6.2
---
Expand Down
27 changes: 26 additions & 1 deletion 27 src/Symfony/Component/HttpFoundation/InputBag.php
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,15 @@ public function getEnum(string $key, string $class, \BackedEnum $default = null)
}
}

/**
* Returns the parameter value converted to string.
fabpot marked this conversation as resolved.
Show resolved Hide resolved
*/
public function getString(string $key, string $default = ''): string
GromNaN marked this conversation as resolved.
Show resolved Hide resolved
{
// Shortcuts the parent method because the validation on scalar is already done in get().
return (string) $this->get($key, $default);
}

public function filter(string $key, mixed $default = null, int $filter = \FILTER_DEFAULT, mixed $options = []): mixed
{
$value = $this->has($key) ? $this->all()[$key] : $default;
Expand All @@ -109,6 +118,22 @@ public function filter(string $key, mixed $default = null, int $filter = \FILTER
throw new \InvalidArgumentException(sprintf('A Closure must be passed to "%s()" when FILTER_CALLBACK is used, "%s" given.', __METHOD__, get_debug_type($options['options'] ?? null)));
}

return filter_var($value, $filter, $options);
$options['flags'] ??= 0;
$nullOnFailure = $options['flags'] & \FILTER_NULL_ON_FAILURE;
$options['flags'] |= \FILTER_NULL_ON_FAILURE;

$value = filter_var($value, $filter, $options);

if (null !== $value || $nullOnFailure) {
return $value;
}

$method = debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS | \DEBUG_BACKTRACE_PROVIDE_OBJECT, 2)[1];
$method = ($method['object'] ?? null) === $this ? $method['function'] : 'filter';
$hint = 'filter' === $method ? 'pass' : 'use method "filter()" with';

trigger_deprecation('symfony/http-foundation', '6.3', 'Ignoring invalid values when using "%s::%s(\'%s\')" is deprecated and will throw a "%s" in 7.0; '.$hint.' flag "FILTER_NULL_ON_FAILURE" to keep ignoring them.', $this::class, $method, $key, BadRequestException::class);

return false;
}
}
50 changes: 42 additions & 8 deletions 50 src/Symfony/Component/HttpFoundation/ParameterBag.php
Original file line number Diff line number Diff line change
Expand Up @@ -114,40 +114,53 @@ public function remove(string $key)
*/
public function getAlpha(string $key, string $default = ''): string
{
return preg_replace('/[^[:alpha:]]/', '', $this->get($key, $default));
return preg_replace('/[^[:alpha:]]/', '', $this->getString($key, $default));
GromNaN marked this conversation as resolved.
Show resolved Hide resolved
nicolas-grekas marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* Returns the alphabetic characters and digits of the parameter value.
*/
public function getAlnum(string $key, string $default = ''): string
{
return preg_replace('/[^[:alnum:]]/', '', $this->get($key, $default));
return preg_replace('/[^[:alnum:]]/', '', $this->getString($key, $default));
}

/**
* Returns the digits of the parameter value.
*/
public function getDigits(string $key, string $default = ''): string
{
// we need to remove - and + because they're allowed in the filter
return str_replace(['-', '+'], '', $this->filter($key, $default, \FILTER_SANITIZE_NUMBER_INT));
return preg_replace('/[^[:digit:]]/', '', $this->getString($key, $default));
}

/**
* Returns the parameter as string.
*/
public function getString(string $key, string $default = ''): string
{
$value = $this->get($key, $default);
if (!\is_scalar($value) && !$value instanceof \Stringable) {
throw new \UnexpectedValueException(sprintf('Parameter value "%s" cannot be converted to "string".', $key));
}

return (string) $value;
}

/**
* Returns the parameter value converted to integer.
*/
public function getInt(string $key, int $default = 0): int
{
return (int) $this->get($key, $default);
// In 7.0 remove the fallback to 0, in case of failure an exception will be thrown
return $this->filter($key, $default, \FILTER_VALIDATE_INT, ['flags' => \FILTER_REQUIRE_SCALAR]) ?: 0;
}

/**
* Returns the parameter value converted to boolean.
*/
public function getBoolean(string $key, bool $default = false): bool
{
return $this->filter($key, $default, \FILTER_VALIDATE_BOOL);
return $this->filter($key, $default, \FILTER_VALIDATE_BOOL, ['flags' => \FILTER_REQUIRE_SCALAR]);
}

/**
Expand Down Expand Up @@ -178,7 +191,8 @@ public function getEnum(string $key, string $class, \BackedEnum $default = null)
/**
* Filter key.
*
* @param int $filter FILTER_* constant
* @param int $filter FILTER_* constant
* @param int|array{flags?: int, options?: array} $options Flags from FILTER_* constants
*
* @see https://php.net/filter-var
*/
Expand All @@ -196,11 +210,31 @@ public function filter(string $key, mixed $default = null, int $filter = \FILTER
$options['flags'] = \FILTER_REQUIRE_ARRAY;
}

if (\is_object($value) && !$value instanceof \Stringable) {
throw new \UnexpectedValueException(sprintf('Parameter value "%s" cannot be filtered.', $key));
}

if ((\FILTER_CALLBACK & $filter) && !(($options['options'] ?? null) instanceof \Closure)) {
throw new \InvalidArgumentException(sprintf('A Closure must be passed to "%s()" when FILTER_CALLBACK is used, "%s" given.', __METHOD__, get_debug_type($options['options'] ?? null)));
}

return filter_var($value, $filter, $options);
$options['flags'] ??= 0;
$nullOnFailure = $options['flags'] & \FILTER_NULL_ON_FAILURE;
$options['flags'] |= \FILTER_NULL_ON_FAILURE;

$value = filter_var($value, $filter, $options);

if (null !== $value || $nullOnFailure) {
return $value;
}

$method = debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS | \DEBUG_BACKTRACE_PROVIDE_OBJECT, 2)[1];
$method = ($method['object'] ?? null) === $this ? $method['function'] : 'filter';
$hint = 'filter' === $method ? 'pass' : 'use method "filter()" with';

trigger_deprecation('symfony/http-foundation', '6.3', 'Ignoring invalid values when using "%s::%s(\'%s\')" is deprecated and will throw an "%s" in 7.0; '.$hint.' flag "FILTER_NULL_ON_FAILURE" to keep ignoring them.', $this::class, $method, $key, \UnexpectedValueException::class);

return false;
}

/**
Expand Down
85 changes: 85 additions & 0 deletions 85 src/Symfony/Component/HttpFoundation/Tests/InputBagTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,15 @@
namespace Symfony\Component\HttpFoundation\Tests;

use PHPUnit\Framework\TestCase;
use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait;
use Symfony\Component\HttpFoundation\Exception\BadRequestException;
use Symfony\Component\HttpFoundation\InputBag;
use Symfony\Component\HttpFoundation\Tests\Fixtures\FooEnum;

class InputBagTest extends TestCase
{
use ExpectDeprecationTrait;

public function testGet()
{
$bag = new InputBag(['foo' => 'bar', 'null' => null, 'int' => 1, 'float' => 1.0, 'bool' => false, 'stringable' => new class() implements \Stringable {
Expand All @@ -36,6 +39,58 @@ public function __toString(): string
$this->assertFalse($bag->get('bool'), '->get() gets the value of a bool parameter');
}

/**
* @group legacy
*/
public function testGetIntError()
{
$this->expectDeprecation('Since symfony/http-foundation 6.3: Ignoring invalid values when using "Symfony\Component\HttpFoundation\InputBag::getInt(\'foo\')" is deprecated and will throw a "Symfony\Component\HttpFoundation\Exception\BadRequestException" in 7.0; use method "filter()" with flag "FILTER_NULL_ON_FAILURE" to keep ignoring them.');

$bag = new InputBag(['foo' => 'bar']);
$result = $bag->getInt('foo');
$this->assertSame(0, $result);
}

/**
* @group legacy
*/
public function testGetBooleanError()
{
$this->expectDeprecation('Since symfony/http-foundation 6.3: Ignoring invalid values when using "Symfony\Component\HttpFoundation\InputBag::getBoolean(\'foo\')" is deprecated and will throw a "Symfony\Component\HttpFoundation\Exception\BadRequestException" in 7.0; use method "filter()" with flag "FILTER_NULL_ON_FAILURE" to keep ignoring them.');

$bag = new InputBag(['foo' => 'bar']);
$result = $bag->getBoolean('foo');
$this->assertFalse($result);
}

public function testGetString()
{
$bag = new InputBag(['integer' => 123, 'bool_true' => true, 'bool_false' => false, 'string' => 'abc', 'stringable' => new class() implements \Stringable {
public function __toString(): string
{
return 'strval';
}
}]);

$this->assertSame('123', $bag->getString('integer'), '->getString() gets a value of parameter as string');
$this->assertSame('abc', $bag->getString('string'), '->getString() gets a value of parameter as string');
$this->assertSame('', $bag->getString('unknown'), '->getString() returns zero if a parameter is not defined');
$this->assertSame('foo', $bag->getString('unknown', 'foo'), '->getString() returns the default if a parameter is not defined');
$this->assertSame('1', $bag->getString('bool_true'), '->getString() returns "1" if a parameter is true');
$this->assertSame('', $bag->getString('bool_false', 'foo'), '->getString() returns an empty empty string if a parameter is false');
$this->assertSame('strval', $bag->getString('stringable'), '->getString() gets a value of a stringable paramater as string');
}

public function testGetStringExceptionWithArray()
{
$bag = new InputBag(['key' => ['abc']]);

$this->expectException(BadRequestException::class);
$this->expectExceptionMessage('Input value "key" contains a non-scalar value.');

$bag->getString('key');
}

public function testGetDoesNotUseDeepByDefault()
{
$bag = new InputBag(['foo' => ['bar' => 'moo']]);
Expand Down Expand Up @@ -126,4 +181,34 @@ public function testGetEnumThrowsExceptionWithInvalidValue()

$this->assertNull($bag->getEnum('invalid-value', FooEnum::class));
}

public function testGetAlnumExceptionWithArray()
{
$bag = new InputBag(['word' => ['foo_BAR_012']]);

$this->expectException(BadRequestException::class);
$this->expectExceptionMessage('Input value "word" contains a non-scalar value.');

$bag->getAlnum('word');
}

public function testGetAlphaExceptionWithArray()
{
$bag = new InputBag(['word' => ['foo_BAR_012']]);

$this->expectException(BadRequestException::class);
$this->expectExceptionMessage('Input value "word" contains a non-scalar value.');

$bag->getAlpha('word');
}

public function testGetDigitsExceptionWithArray()
{
$bag = new InputBag(['word' => ['foo_BAR_012']]);

$this->expectException(BadRequestException::class);
$this->expectExceptionMessage('Input value "word" contains a non-scalar value.');

$bag->getDigits('word');
}
}
Loading
Morty Proxy This is a proxified and sanitized view of the page, visit original site.