diff --git a/src/Symfony/Component/Form/Extension/HttpFoundation/HttpFoundationRequestHandler.php b/src/Symfony/Component/Form/Extension/HttpFoundation/HttpFoundationRequestHandler.php index a58ad246ea32..47ce62feefe1 100644 --- a/src/Symfony/Component/Form/Extension/HttpFoundation/HttpFoundationRequestHandler.php +++ b/src/Symfony/Component/Form/Extension/HttpFoundation/HttpFoundationRequestHandler.php @@ -63,7 +63,7 @@ public function handleRequest(FormInterface $form, $request = null) return; } - $data = $request->query->get($name); + $data = $request->query->all()[$name]; } } else { // Mark the form with an error if the uploaded size was too large @@ -87,7 +87,7 @@ public function handleRequest(FormInterface $form, $request = null) $files = $request->files->all(); } elseif ($request->request->has($name) || $request->files->has($name)) { $default = $form->getConfig()->getCompound() ? [] : null; - $params = $request->request->get($name, $default); + $params = $request->request->all()[$name] ?? $default; $files = $request->files->get($name, $default); } else { // Don't submit the form if it is not present in the request diff --git a/src/Symfony/Component/HttpFoundation/CHANGELOG.md b/src/Symfony/Component/HttpFoundation/CHANGELOG.md index e6ea6811e82c..60fab8381bf4 100644 --- a/src/Symfony/Component/HttpFoundation/CHANGELOG.md +++ b/src/Symfony/Component/HttpFoundation/CHANGELOG.md @@ -16,6 +16,8 @@ CHANGELOG * added `MarshallingSessionHandler`, `IdentityMarshaller` * made `Session` accept a callback to report when the session is being used * Add support for all core cache control directives + * Added `Symfony\Component\HttpFoundation\InputBag` + * Deprecated retrieving non-string values using `InputBag::get()`, use `InputBag::all()` if you need access to the collection of values 5.0.0 ----- diff --git a/src/Symfony/Component/HttpFoundation/Exception/BadRequestException.php b/src/Symfony/Component/HttpFoundation/Exception/BadRequestException.php new file mode 100644 index 000000000000..e4bb309c42ab --- /dev/null +++ b/src/Symfony/Component/HttpFoundation/Exception/BadRequestException.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Exception; + +/** + * Raised when a user sends a malformed request. + */ +class BadRequestException extends \UnexpectedValueException implements RequestExceptionInterface +{ +} diff --git a/src/Symfony/Component/HttpFoundation/InputBag.php b/src/Symfony/Component/HttpFoundation/InputBag.php new file mode 100644 index 000000000000..b0a9ef35982e --- /dev/null +++ b/src/Symfony/Component/HttpFoundation/InputBag.php @@ -0,0 +1,119 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation; + +use Symfony\Component\HttpFoundation\Exception\BadRequestException; + +/** + * InputBag is a container for user input values such as $_GET, $_POST, $_REQUEST, and $_COOKIE. + * + * @author Saif Eddin Gmati + */ +final class InputBag extends ParameterBag +{ + /** + * Returns a string input value by name. + * + * @param string|null $default The default value if the input key does not exist + * + * @return string|null + */ + public function get(string $key, $default = null) + { + if (null !== $default && !is_scalar($default) && !method_exists($default, '__toString')) { + trigger_deprecation('symfony/http-foundation', '5.1', 'Passing a non-string value as 2nd argument to "%s()" is deprecated, pass a string or null instead.', __METHOD__); + } + + $value = parent::get($key, $this); + + if (null !== $value && $this !== $value && !is_scalar($value) && !method_exists($value, '__toString')) { + trigger_deprecation('symfony/http-foundation', '5.1', 'Retrieving a non-string value from "%s()" is deprecated, and will throw a "%s" exception in Symfony 6.0, use "%s::all()" instead.', __METHOD__, BadRequestException::class, __CLASS__); + } + + return $this === $value ? $default : $value; + } + + /** + * Returns the inputs. + * + * @param string|null $key The name of the input to return or null to get them all + */ + public function all(string $key = null): array + { + if (null === $key) { + return $this->parameters; + } + + $value = $this->parameters[$key] ?? []; + if (!\is_array($value)) { + throw new BadRequestException(sprintf('Unexpected value for "%s" input, expecting "array", got "%s".', $key, get_debug_type($value))); + } + + return $value; + } + + /** + * Replaces the current input values by a new set. + */ + public function replace(array $inputs = []) + { + $this->parameters = []; + $this->add($inputs); + } + + /** + * Adds input values. + */ + public function add(array $inputs = []) + { + foreach ($inputs as $input => $value) { + $this->set($input, $value); + } + } + + /** + * Sets an input by name. + * + * @param string|array $value + */ + public function set(string $key, $value) + { + if (!is_scalar($value) && !method_exists($value, '__toString') && !\is_array($value)) { + trigger_deprecation('symfony/http-foundation', '5.1', 'Passing "%s" as a 2nd Argument to "%s()" is deprecated, pass a string or an array instead.', get_debug_type($value), __METHOD__); + } + + $this->parameters[$key] = $value; + } + + /** + * {@inheritdoc} + */ + public function filter(string $key, $default = null, int $filter = FILTER_DEFAULT, $options = []) + { + $value = $this->has($key) ? $this->all()[$key] : $default; + + // Always turn $options into an array - this allows filter_var option shortcuts. + if (!\is_array($options) && $options) { + $options = ['flags' => $options]; + } + + if (\is_array($value) && !(($options['flags'] ?? 0) & (FILTER_REQUIRE_ARRAY | FILTER_FORCE_ARRAY))) { + trigger_deprecation('symfony/http-foundation', '5.1', 'Filtering an array value with "%s()" without passing the FILTER_REQUIRE_ARRAY or FILTER_FORCE_ARRAY flag is deprecated', __METHOD__); + + if (!isset($options['flags'])) { + $options['flags'] = FILTER_REQUIRE_ARRAY; + } + } + + return filter_var($value, $filter, $options); + } +} diff --git a/src/Symfony/Component/HttpFoundation/Request.php b/src/Symfony/Component/HttpFoundation/Request.php index eac076e2c009..7363fc467f59 100644 --- a/src/Symfony/Component/HttpFoundation/Request.php +++ b/src/Symfony/Component/HttpFoundation/Request.php @@ -84,14 +84,14 @@ class Request /** * Request body parameters ($_POST). * - * @var ParameterBag + * @var InputBag */ public $request; /** * Query string parameters ($_GET). * - * @var ParameterBag + * @var InputBag */ public $query; @@ -112,7 +112,7 @@ class Request /** * Cookies ($_COOKIE). * - * @var ParameterBag + * @var InputBag */ public $cookies; @@ -267,10 +267,10 @@ public function __construct(array $query = [], array $request = [], array $attri */ public function initialize(array $query = [], array $request = [], array $attributes = [], array $cookies = [], array $files = [], array $server = [], $content = null) { - $this->request = new ParameterBag($request); - $this->query = new ParameterBag($query); + $this->request = new InputBag($request); + $this->query = new InputBag($query); $this->attributes = new ParameterBag($attributes); - $this->cookies = new ParameterBag($cookies); + $this->cookies = new InputBag($cookies); $this->files = new FileBag($files); $this->server = new ServerBag($server); $this->headers = new HeaderBag($this->server->getHeaders()); @@ -301,7 +301,7 @@ public static function createFromGlobals() && \in_array(strtoupper($request->server->get('REQUEST_METHOD', 'GET')), ['PUT', 'DELETE', 'PATCH']) ) { parse_str($request->getContent(), $data); - $request->request = new ParameterBag($data); + $request->request = new InputBag($data); } return $request; @@ -443,16 +443,16 @@ public function duplicate(array $query = null, array $request = null, array $att { $dup = clone $this; if (null !== $query) { - $dup->query = new ParameterBag($query); + $dup->query = new InputBag($query); } if (null !== $request) { - $dup->request = new ParameterBag($request); + $dup->request = new InputBag($request); } if (null !== $attributes) { $dup->attributes = new ParameterBag($attributes); } if (null !== $cookies) { - $dup->cookies = new ParameterBag($cookies); + $dup->cookies = new InputBag($cookies); } if (null !== $files) { $dup->files = new FileBag($files); @@ -708,12 +708,12 @@ public function get(string $key, $default = null) return $result; } - if ($this !== $result = $this->query->get($key, $this)) { - return $result; + if ($this->query->has($key)) { + return $this->query->all()[$key]; } - if ($this !== $result = $this->request->get($key, $this)) { - return $result; + if ($this->request->has($key)) { + return $this->request->all()[$key]; } return $default; @@ -1564,8 +1564,8 @@ public function isNoCache() /** * Gets the preferred format for the response by inspecting, in the following order: - * * the request format set using setRequestFormat - * * the values of the Accept HTTP header + * * the request format set using setRequestFormat; + * * the values of the Accept HTTP header. * * Note that if you use this method, you should send the "Vary: Accept" header * in the response to prevent any issues with intermediary HTTP caches. diff --git a/src/Symfony/Component/HttpFoundation/Tests/InputBagTest.php b/src/Symfony/Component/HttpFoundation/Tests/InputBagTest.php new file mode 100644 index 000000000000..febe5eda62f0 --- /dev/null +++ b/src/Symfony/Component/HttpFoundation/Tests/InputBagTest.php @@ -0,0 +1,103 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +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; + +class InputBagTest extends TestCase +{ + use ExpectDeprecationTrait; + + public function testGet() + { + $bag = new InputBag(['foo' => 'bar', 'null' => null]); + + $this->assertEquals('bar', $bag->get('foo'), '->get() gets the value of a parameter'); + $this->assertEquals('default', $bag->get('unknown', 'default'), '->get() returns second argument as default if a parameter is not defined'); + $this->assertNull($bag->get('null', 'default'), '->get() returns null if null is set'); + } + + public function testGetDoesNotUseDeepByDefault() + { + $bag = new InputBag(['foo' => ['bar' => 'moo']]); + + $this->assertNull($bag->get('foo[bar]')); + } + + public function testAllWithInputKey() + { + $bag = new InputBag(['foo' => ['bar', 'baz'], 'null' => null]); + + $this->assertEquals(['bar', 'baz'], $bag->all('foo'), '->all() gets the value of a parameter'); + $this->assertEquals([], $bag->all('unknown'), '->all() returns an empty array if a parameter is not defined'); + } + + public function testAllThrowsForNonArrayValues() + { + $this->expectException(BadRequestException::class); + $bag = new InputBag(['foo' => 'bar', 'null' => null]); + $bag->all('foo'); + } + + public function testFilterArray() + { + $bag = new InputBag([ + 'foo' => ['12', '8'], + ]); + + $result = $bag->filter('foo', null, \FILTER_VALIDATE_INT, \FILTER_FORCE_ARRAY); + $this->assertSame([12, 8], $result); + } + + /** + * @group legacy + */ + public function testSetWithNonStringishOrArrayIsDeprecated() + { + $bag = new InputBag(); + $this->expectDeprecation('Since symfony/http-foundation 5.1: Passing "Symfony\Component\HttpFoundation\InputBag" as a 2nd Argument to "Symfony\Component\HttpFoundation\InputBag::set()" is deprecated, pass a string or an array instead.'); + $bag->set('foo', new InputBag()); + } + + /** + * @group legacy + */ + public function testGettingANonStringValueIsDeprecated() + { + $bag = new InputBag(['foo' => ['a', 'b']]); + $this->expectDeprecation('Since symfony/http-foundation 5.1: Retrieving a non-string value from "Symfony\Component\HttpFoundation\InputBag::get()" is deprecated, and will throw a "Symfony\Component\HttpFoundation\Exception\BadRequestException" exception in Symfony 6.0, use "Symfony\Component\HttpFoundation\InputBag::all()" instead.'); + $bag->get('foo'); + } + + /** + * @group legacy + */ + public function testGetWithNonStringDefaultValueIsDeprecated() + { + $bag = new InputBag(['foo' => 'bar']); + $this->expectDeprecation('Since symfony/http-foundation 5.1: Passing a non-string value as 2nd argument to "Symfony\Component\HttpFoundation\InputBag::get()" is deprecated, pass a string or null instead.'); + $bag->get('foo', ['a', 'b']); + } + + /** + * @group legacy + */ + public function testFilterArrayWithoutArrayFlagIsDeprecated() + { + $bag = new InputBag(['foo' => ['bar', 'baz']]); + $this->expectDeprecation('Since symfony/http-foundation 5.1: Filtering an array value with "Symfony\Component\HttpFoundation\InputBag::filter()" without passing the FILTER_REQUIRE_ARRAY or FILTER_FORCE_ARRAY flag is deprecated'); + $bag->filter('foo', \FILTER_VALIDATE_INT); + } +} diff --git a/src/Symfony/Component/Security/Http/ParameterBagUtils.php b/src/Symfony/Component/Security/Http/ParameterBagUtils.php index 88312f01313b..db7ac6e10776 100644 --- a/src/Symfony/Component/Security/Http/ParameterBagUtils.php +++ b/src/Symfony/Component/Security/Http/ParameterBagUtils.php @@ -36,12 +36,12 @@ final class ParameterBagUtils public static function getParameterBagValue(ParameterBag $parameters, string $path) { if (false === $pos = strpos($path, '[')) { - return $parameters->get($path); + return $parameters->all()[$path] ?? null; } $root = substr($path, 0, $pos); - if (null === $value = $parameters->get($root)) { + if (null === $value = $parameters->all()[$root] ?? null) { return null; }