From 36062df690a4c6f38d85f11883d82ce85437cc9f Mon Sep 17 00:00:00 2001 From: daniel Date: Tue, 27 Feb 2024 13:28:00 +0100 Subject: [PATCH] RequestInterface: add hasBody() hasBody() provides an easy way to check whether the request contains payload data. This ensures this check is performed consistently and in accordance with RFC 9110. Change-Id: If15f2cb6239c4c8170e70a5bec8a5f0821361275 --- includes/Rest/RequestBase.php | 30 +++++++++++++ includes/Rest/RequestData.php | 17 ++++++++ includes/Rest/RequestInterface.php | 8 ++++ .../unit/includes/Rest/RequestBaseTest.php | 30 +++++++++++++ .../unit/includes/Rest/RequestDataTest.php | 43 +++++++++++++++++++ 5 files changed, 128 insertions(+) diff --git a/includes/Rest/RequestBase.php b/includes/Rest/RequestBase.php index 7445e711bdb..62d66450ad6 100644 --- a/includes/Rest/RequestBase.php +++ b/includes/Rest/RequestBase.php @@ -128,4 +128,34 @@ abstract class RequestBase implements RequestInterface { } return $ct; } + + /** + * Return true if the client provided a content-length header or a + * transfer-encoding header. + * + * @see https://www.rfc-editor.org/rfc/rfc9110.html#name-content-length + * + * @return bool + */ + public function hasBody(): bool { + // From RFC9110, section 8.6: A user agent SHOULD send Content-Length + // in a request when the method defines a meaning for enclosed content + // and it is not sending Transfer-Encoding. [...] + // A user agent SHOULD NOT send a Content-Length header field when the + // request message does not contain content and the method semantics do + // not anticipate such data. + + if ( $this->getHeaderLine( 'content-length' ) !== '' ) { + // If a content length is set, there is a body + return true; + } + + if ( $this->getHeaderLine( 'transfer-encoding' ) !== '' ) { + // If a transfer encoding is set, there is a body + return true; + } + + return false; + } + } diff --git a/includes/Rest/RequestData.php b/includes/Rest/RequestData.php index 59469865ef5..4e8026eee81 100644 --- a/includes/Rest/RequestData.php +++ b/includes/Rest/RequestData.php @@ -102,4 +102,21 @@ class RequestData extends RequestBase { public function getPostParams() { return $this->postParams; } + + public function hasBody(): bool { + if ( parent::hasBody() ) { + return true; + } + + if ( $this->parsedBody !== null ) { + return true; + } + + if ( $this->getBody()->getContents() !== '' ) { + return true; + } + + return false; + } + } diff --git a/includes/Rest/RequestInterface.php b/includes/Rest/RequestInterface.php index 596da4eebe9..a813b3c7d16 100644 --- a/includes/Rest/RequestInterface.php +++ b/includes/Rest/RequestInterface.php @@ -268,4 +268,12 @@ interface RequestInterface { public function setParsedBody( ?array $data ); public function getBodyType(): ?string; + + /** + * Determines whether the request has body data associated with it. + * Note that this method may return true even if the body is empty. + * + * @return bool + */ + public function hasBody(): bool; } diff --git a/tests/phpunit/unit/includes/Rest/RequestBaseTest.php b/tests/phpunit/unit/includes/Rest/RequestBaseTest.php index 28c907b602c..b988c382e8b 100644 --- a/tests/phpunit/unit/includes/Rest/RequestBaseTest.php +++ b/tests/phpunit/unit/includes/Rest/RequestBaseTest.php @@ -64,4 +64,34 @@ class RequestBaseTest extends \MediaWikiUnitTestCase { $rb->setHeaders( [ 'Content-type' => 'application/json' ] ); $this->assertSame( [ 'application/json' ], $rb->getHeaders()[ 'Content-type' ] ); } + + public static function provideHasBody() { + yield 'nothing' + => [ [], false ]; + + yield 'content-length: 1' + => [ [ 'content-length' => '1' ], true ]; + + yield 'content-length: 0' + => [ [ 'content-length' => '0' ], true ]; + + yield 'content-length empty' + => [ [ 'content-length' => '' ], false ]; + + yield 'transfer-encoding: chunked' + => [ [ 'transfer-encoding' => 'chunked' ], true ]; + + yield 'transfer-encoding empty' + => [ [ 'transfer-encoding' => '' ], false ]; + } + + /** + * @dataProvider provideHasBody + */ + public function testHasBody( $headers, $expected ) { + $rb = $this->getMockForAbstractClass( RequestBase::class, [ 'cookiePrefix' ] ); + $rb->setHeaders( $headers ); + $this->assertSame( $expected, $rb->hasBody() ); + } + } diff --git a/tests/phpunit/unit/includes/Rest/RequestDataTest.php b/tests/phpunit/unit/includes/Rest/RequestDataTest.php index 315f3c08e28..cb9211c974a 100644 --- a/tests/phpunit/unit/includes/Rest/RequestDataTest.php +++ b/tests/phpunit/unit/includes/Rest/RequestDataTest.php @@ -221,4 +221,47 @@ class RequestDataTest extends \MediaWikiUnitTestCase { $this->assertSame( $expectedResult, $request->getBodyType() ); } + public static function provideHasBody() { + yield 'nothing' + => [ [], false ]; + + yield 'content-length: 1' + => [ [ 'content-length' => '1' ], true ]; + + yield 'content-length: 0' + => [ [ 'content-length' => '0' ], true ]; + + yield 'content-length empty' + => [ [ 'content-length' => '' ], false ]; + + yield 'transfer-encoding: chunked' + => [ [ 'transfer-encoding' => 'chunked' ], true ]; + + yield 'transfer-encoding empty' + => [ [ 'transfer-encoding' => '' ], false ]; + } + + /** + * @dataProvider provideHasBody + */ + public function testHasBodyBasedOnHeader( $headers, $expected ) { + $request = new RequestData( [ + 'headers' => $headers + ] ); + $this->assertSame( $expected, $request->hasBody() ); + } + + public function testHasBodyWithContent() { + $request = new RequestData( [ + 'bodyContents' => 'test test test' + ] ); + $this->assertTrue( $request->hasBody() ); + } + + public function testHasBodyWithParsedBody() { + $request = new RequestData( [ + 'parsedBody' => [ 'foo' => 'bar' ] + ] ); + $this->assertTrue( $request->hasBody() ); + } }