Skip to content
Open
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
1 change: 1 addition & 0 deletions deptrac.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@ deptrac:
- Cookie
- Files
- I18n
- Input
- Security
- URI
Images:
Expand Down
54 changes: 54 additions & 0 deletions system/HTTP/IncomingRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
use CodeIgniter\HTTP\Exceptions\HTTPException;
use CodeIgniter\HTTP\Files\FileCollection;
use CodeIgniter\HTTP\Files\UploadedFile;
use CodeIgniter\Input\InputData;
use Config\App;
use Config\Services;
use Locale;
Expand Down Expand Up @@ -555,6 +556,59 @@ public function getRawInputVar($index = null, ?int $filter = null, $flags = null
return $output;
}

/**
* Returns query-string parameters as a typed input object.
*/
public function getQueryInput(): InputData
{
$data = $this->getGet();

return service('inputdatafactory')->create(is_array($data) ? $data : []);
}

/**
* Returns POST body parameters as a typed input object.
*/
public function getPostInput(): InputData
{
$data = $this->getPost();

return service('inputdatafactory')->create(is_array($data) ? $data : []);
}

/**
* Returns request body payload parameters as a typed input object.
*/
public function getPayloadInput(): InputData
{
$contentType = $this->getHeaderLine('Content-Type');

if (str_contains($contentType, 'application/json')) {
$data = $this->getJSON(true) ?? [];

if (! is_array($data)) {
throw HTTPException::forUnsupportedJSONFormat();
}

return service('inputdatafactory')->create($data);
}

if (
in_array($this->getMethod(), [Method::PUT, Method::PATCH, Method::DELETE], true)
&& ! str_contains($contentType, 'multipart/form-data')
) {
return service('inputdatafactory')->create($this->getRawInput());
}

if (in_array($this->getMethod(), [Method::GET, Method::HEAD], true)) {
return service('inputdatafactory')->create([]);
}

$data = $this->getPost();

return service('inputdatafactory')->create(is_array($data) ? $data : []);
}

/**
* Fetch an item from GET data.
*
Expand Down
128 changes: 128 additions & 0 deletions tests/system/HTTP/IncomingRequestTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
use CodeIgniter\Exceptions\InvalidArgumentException;
use CodeIgniter\HTTP\Exceptions\HTTPException;
use CodeIgniter\HTTP\Files\UploadedFile;
use CodeIgniter\Input\InputData;
use CodeIgniter\Superglobals;
use CodeIgniter\Test\CIUnitTestCase;
use Config\App;
Expand Down Expand Up @@ -86,6 +87,35 @@ public function testCanGrabPostVars(): void
$this->assertNull($this->request->getPost('TESTY'));
}

public function testGetQueryInputReadsQueryData(): void
{
service('superglobals')->setGet('page', '3');
service('superglobals')->setGet('filters', ['active' => 'true']);
service('superglobals')->setPost('page', '10');

$request = $this->createRequest();
$input = $request->getQueryInput();

$this->assertInstanceOf(InputData::class, $input);
$this->assertSame(3, $input->integer('page'));
$this->assertTrue($input->boolean('filters.active'));
$this->assertSame(1, $input->integer('missing', 1));
}

public function testGetPostInputReadsPostData(): void
{
service('superglobals')->setGet('remember', '0');
service('superglobals')->setPost('remember', '1');
service('superglobals')->setPost('tags', ['php', 'ci4']);

$request = $this->createRequest();
$input = $request->getPostInput();

$this->assertInstanceOf(InputData::class, $input);
$this->assertTrue($input->boolean('remember'));
$this->assertSame(['php', 'ci4'], $input->array('tags'));
}

public function testCanGrabPostBeforeGet(): void
{
service('superglobals')->setPost('TEST', '5');
Expand Down Expand Up @@ -572,6 +602,104 @@ public function testCanGrabGetRawInput(): void
$this->assertSame($expected, $request->getRawInput());
}

public function testGetPayloadInputReadsJsonBody(): void
{
$json = json_encode([
'page' => '4',
'filters' => ['active' => 'true'],
'nullable' => null,
]);

$request = $this->createRequest(new App(), $json);
$request->setHeader('Content-Type', 'application/json');

$input = $request->getPayloadInput();

$this->assertInstanceOf(InputData::class, $input);
$this->assertSame(4, $input->integer('page'));
$this->assertTrue($input->boolean('filters.active'));
$this->assertTrue($input->has('nullable'));
}

#[DataProvider('provideGetPayloadInputReadsRawBodyForWriteRequests')]
public function testGetPayloadInputReadsRawBodyForWriteRequests(string $method): void
{
$request = $this->createRequest(new App(), 'title=Hello&published=1')
->withMethod($method);

$input = $request->getPayloadInput();

$this->assertSame('Hello', $input->string('title'));
$this->assertTrue($input->boolean('published'));
}

/**
* @return iterable<string, array{string}>
*/
public static function provideGetPayloadInputReadsRawBodyForWriteRequests(): iterable
{
yield 'PUT' => ['PUT'];

yield 'PATCH' => ['PATCH'];

yield 'DELETE' => ['DELETE'];
}

public function testGetPayloadInputReadsPostBodyForPostRequests(): void
{
service('superglobals')->setGet('title', 'Query title');
service('superglobals')->setPost('title', 'Post title');

$request = $this->createRequest()->withMethod('POST');
$input = $request->getPayloadInput();

$this->assertSame('Post title', $input->string('title'));
}

public function testGetPayloadInputDoesNotReadQueryDataForGetRequests(): void
{
service('superglobals')->setGet('page', '2');

$request = $this->createRequest()->withMethod('GET');
$input = $request->getPayloadInput();

$this->assertFalse($input->has('page'));
$this->assertSame(1, $input->integer('page', 1));
}

public function testGetPayloadInputReturnsEmptyInputForEmptyJsonBody(): void
{
$request = $this->createRequest(new App());
$request->setHeader('Content-Type', 'application/json');

$input = $request->getPayloadInput();

$this->assertInstanceOf(InputData::class, $input);
$this->assertFalse($input->has('name'));
}

public function testGetPayloadInputRejectsScalarJsonBody(): void
{
$this->expectException(HTTPException::class);
$this->expectExceptionMessage('The provided JSON format is not supported.');

$request = $this->createRequest(new App(), '"hello"');
$request->setHeader('Content-Type', 'application/json');

$request->getPayloadInput();
}

public function testGetPayloadInputKeepsInvalidJsonError(): void
{
$this->expectException(HTTPException::class);
$this->expectExceptionMessage('Failed to parse JSON string. Error: Syntax error');

$request = $this->createRequest(new App(), 'Invalid JSON string');
$request->setHeader('Content-Type', 'application/json');

$request->getPayloadInput();
}

/**
* @param string $rawstring
* @param mixed $var
Expand Down
1 change: 1 addition & 0 deletions user_guide_src/source/changelogs/v4.8.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,7 @@ HTTP

- Added the ``retry`` option to ``CURLRequest`` for retrying failed responses with configurable delays, retryable status codes, optional transient cURL error retries, and ``Retry-After`` support. See :ref:`curlrequest-request-options-retry`.
- Added :ref:`Form Requests <form-requests>` - a new ``FormRequest`` base class that encapsulates validation rules, custom error messages, and authorization logic for a single HTTP request.
- Added ``IncomingRequest::getQueryInput()``, ``getPostInput()``, and ``getPayloadInput()`` to read source-specific request data through ``InputData``.
- Added ``SSEResponse`` class for streaming Server-Sent Events (SSE) over HTTP. See :ref:`server-sent-events`.
- ``Response`` and its child classes no longer require ``Config\App`` passed to their constructors.
Consequently, ``CURLRequest``'s ``$config`` parameter is unused and will be removed in a future release.
Expand Down
42 changes: 41 additions & 1 deletion user_guide_src/source/incoming/incomingrequest.rst
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,32 @@ The ``getVar()`` method will pull from ``$_REQUEST``, so will return any data fr
.. note:: If the incoming request has a ``Content-Type`` header set to ``application/json``,
the ``getVar()`` method returns the JSON data instead of ``$_REQUEST`` data.

.. _incomingrequest-typed-source-input:

Typed Source Input
==================

.. versionadded:: 4.8.0

``getQueryInput()``, ``getPostInput()``, and ``getPayloadInput()`` return
request data as a ``CodeIgniter\Input\InputData`` object. Use these methods
when you want source-explicit access with typed fallback helpers:

.. literalinclude:: incomingrequest/046.php
:lines: 2-

``getQueryInput()`` reads query-string parameters. ``getPostInput()`` reads
POST body parameters. ``getPayloadInput()`` reads the request body payload:
JSON requests use the decoded JSON body, ``PUT``, ``PATCH``, and ``DELETE``
requests use ``getRawInput()`` when they are not multipart requests, and
ordinary form requests use POST body parameters.
For non-JSON ``GET`` and ``HEAD`` requests, use ``getQueryInput()``;
``getPayloadInput()`` returns an empty input object.

These methods do not validate input. They are fallback-friendly helpers for
reading raw request data. Use Validation or :ref:`form-requests` when input
must satisfy application rules before it is consumed.

.. _incomingrequest-getting-json-data:

Getting JSON Data
Expand Down Expand Up @@ -406,6 +432,11 @@ The methods provided by the parent classes that are available are:

.. literalinclude:: incomingrequest/045.php

.. php:method:: getQueryInput()

:returns: Query-string parameters as a typed input object.
:rtype: CodeIgniter\\Input\\InputData

.. php:method:: getPost([$index = null[, $filter = null[, $flags = null]]])

:param string $index: The name of the variable/key to look for.
Expand All @@ -418,6 +449,16 @@ The methods provided by the parent classes that are available are:

This method is identical to ``getGet()``, only it fetches POST data.

.. php:method:: getPostInput()

:returns: POST body parameters as a typed input object.
:rtype: CodeIgniter\\Input\\InputData

.. php:method:: getPayloadInput()

:returns: Request body payload parameters as a typed input object.
:rtype: CodeIgniter\\Input\\InputData

.. php:method:: getPostGet([$index = null[, $filter = null[, $flags = null]]])

:param string $index: The name of the variable/key to look for.
Expand Down Expand Up @@ -519,4 +560,3 @@ The methods provided by the parent classes that are available are:
.. note:: Prior to v4.4.0, this was the safest method to determine the
"current URI", since ``IncomingRequest::$uri`` might not be aware of
the complete App configuration for base URLs.

5 changes: 5 additions & 0 deletions user_guide_src/source/incoming/incomingrequest/046.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?php

$page = $request->getQueryInput()->integer('page', 1);
$remember = $request->getPostInput()->boolean('remember', false);
$name = $request->getPayloadInput()->string('name');
Loading