Skip to content

Commit

Permalink
feature #34363 [HttpFoundation] Add InputBag (azjezz)
Browse files Browse the repository at this point in the history
This PR was merged into the 5.1-dev branch.

Discussion
----------

[HttpFoundation] Add InputBag

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | no
| New feature?  | yes
| Deprecations? | yes
| License       | MIT

When ppl read a request attribute, they never check if an array is returned
This means many apps just fail with a 500 when adding `[]` in the query string.
This PR turns them to 400 basically (with a deprecation for now)

Commits
-------

0a2ef70 [HttpFoundation] add InputBag
  • Loading branch information
fabpot committed Apr 13, 2020
2 parents d5fd44d + 0a2ef70 commit b2f210f
Show file tree
Hide file tree
Showing 7 changed files with 263 additions and 20 deletions.
Expand Up @@ -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
Expand All @@ -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
Expand Down
2 changes: 2 additions & 0 deletions src/Symfony/Component/HttpFoundation/CHANGELOG.md
Expand Up @@ -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
-----
Expand Down
@@ -0,0 +1,19 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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
{
}
119 changes: 119 additions & 0 deletions src/Symfony/Component/HttpFoundation/InputBag.php
@@ -0,0 +1,119 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <saif.gmati@symfony.com>
*/
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);
}
}
32 changes: 16 additions & 16 deletions src/Symfony/Component/HttpFoundation/Request.php
Expand Up @@ -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;

Expand All @@ -112,7 +112,7 @@ class Request
/**
* Cookies ($_COOKIE).
*
* @var ParameterBag
* @var InputBag
*/
public $cookies;

Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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.
Expand Down
103 changes: 103 additions & 0 deletions src/Symfony/Component/HttpFoundation/Tests/InputBagTest.php
@@ -0,0 +1,103 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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);
}
}
4 changes: 2 additions & 2 deletions src/Symfony/Component/Security/Http/ParameterBagUtils.php
Expand Up @@ -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;
}

Expand Down

0 comments on commit b2f210f

Please sign in to comment.