Skip to content

Commit 6d196b2

Browse files
ENGCOM-4474: Fix for issue #21299. Change HEAD action mapping to GET action interface and add HEAD request handling #21378
- Merge Pull Request #21378 from mattijv/magento2:2.3-develop - Merged commits: 1. 46191d8 2. d8eb160 3. 72630cd 4. 4f7db3a 5. e198914 6. 22b9e56 7. 69bd2dd 8. ce2e4d3 9. 56d4cc2 10. 2b58789 11. 5b79a30 12. 6bac208 13. 880404c 14. bc3ef7a 15. be77120 16. fecce53 17. 3d19c09 18. 2a337f9 19. b5683e8 20. 740e4bd
2 parents e05cfbe + 740e4bd commit 6d196b2

File tree

6 files changed

+263
-59
lines changed

6 files changed

+263
-59
lines changed

app/etc/di.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1740,7 +1740,7 @@
17401740
<argument name="map" xsi:type="array">
17411741
<item name="OPTIONS" xsi:type="string">\Magento\Framework\App\Action\HttpOptionsActionInterface</item>
17421742
<item name="GET" xsi:type="string">\Magento\Framework\App\Action\HttpGetActionInterface</item>
1743-
<item name="HEAD" xsi:type="string">\Magento\Framework\App\Action\HttpHeadActionInterface</item>
1743+
<item name="HEAD" xsi:type="string">\Magento\Framework\App\Action\HttpGetActionInterface</item>
17441744
<item name="POST" xsi:type="string">\Magento\Framework\App\Action\HttpPostActionInterface</item>
17451745
<item name="PUT" xsi:type="string">\Magento\Framework\App\Action\HttpPutActionInterface</item>
17461746
<item name="PATCH" xsi:type="string">\Magento\Framework\App\Action\HttpPatchActionInterface</item>

lib/internal/Magento/Framework/App/Action/HttpHeadActionInterface.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212

1313
/**
1414
* Marker for actions processing HEAD requests.
15+
*
16+
* @deprecated Both GET and HEAD requests map to HttpGetActionInterface
1517
*/
1618
interface HttpHeadActionInterface extends ActionInterface
1719
{

lib/internal/Magento/Framework/App/Http.php

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,18 @@
33
* Copyright © Magento, Inc. All rights reserved.
44
* See COPYING.txt for license details.
55
*/
6+
67
namespace Magento\Framework\App;
78

89
use Magento\Framework\App\Filesystem\DirectoryList;
9-
use Magento\Framework\Debug;
10-
use Magento\Framework\ObjectManager\ConfigLoaderInterface;
1110
use Magento\Framework\App\Request\Http as RequestHttp;
1211
use Magento\Framework\App\Response\Http as ResponseHttp;
1312
use Magento\Framework\App\Response\HttpInterface;
1413
use Magento\Framework\Controller\ResultInterface;
14+
use Magento\Framework\Debug;
1515
use Magento\Framework\Event;
1616
use Magento\Framework\Filesystem;
17+
use Magento\Framework\ObjectManager\ConfigLoaderInterface;
1718

1819
/**
1920
* HTTP web application. Called from webroot index.php to serve web requests.
@@ -143,12 +144,31 @@ public function launch()
143144
} else {
144145
throw new \InvalidArgumentException('Invalid return type');
145146
}
147+
if ($this->_request->isHead() && $this->_response->getHttpResponseCode() == 200) {
148+
$this->handleHeadRequest();
149+
}
146150
// This event gives possibility to launch something before sending output (allow cookie setting)
147151
$eventParams = ['request' => $this->_request, 'response' => $this->_response];
148152
$this->_eventManager->dispatch('controller_front_send_response_before', $eventParams);
149153
return $this->_response;
150154
}
151155

156+
/**
157+
* Handle HEAD requests by adding the Content-Length header and removing the body from the response.
158+
*
159+
* @return void
160+
*/
161+
private function handleHeadRequest()
162+
{
163+
// It is possible that some PHP installations have overloaded strlen to use mb_strlen instead.
164+
// This means strlen might return the actual number of characters in a non-ascii string instead
165+
// of the number of bytes. Use mb_strlen explicitly with a single byte character encoding to ensure
166+
// that the content length is calculated in bytes.
167+
$contentLength = mb_strlen($this->_response->getContent(), '8bit');
168+
$this->_response->clearBody();
169+
$this->_response->setHeader('Content-Length', $contentLength);
170+
}
171+
152172
/**
153173
* @inheritdoc
154174
*/
@@ -248,7 +268,7 @@ private function redirectToSetup(Bootstrap $bootstrap, \Exception $exception)
248268
. "because the Magento setup directory cannot be accessed. \n"
249269
. 'You can install Magento using either the command line or you must restore access '
250270
. 'to the following directory: ' . $setupInfo->getDir($projectRoot) . "\n";
251-
271+
// phpcs:ignore Magento2.Exceptions.DirectThrow
252272
throw new \Exception($newMessage, 0, $exception);
253273
}
254274
}
@@ -264,6 +284,7 @@ private function handleBootstrapErrors(Bootstrap $bootstrap, \Exception &$except
264284
{
265285
$bootstrapCode = $bootstrap->getErrorCode();
266286
if (Bootstrap::ERR_MAINTENANCE == $bootstrapCode) {
287+
// phpcs:ignore Magento2.Security.IncludeFile
267288
require $this->_filesystem->getDirectoryRead(DirectoryList::PUB)->getAbsolutePath('errors/503.php');
268289
return true;
269290
}
@@ -304,6 +325,7 @@ private function handleInitException(\Exception $exception)
304325
{
305326
if ($exception instanceof \Magento\Framework\Exception\State\InitException) {
306327
$this->getLogger()->critical($exception);
328+
// phpcs:ignore Magento2.Security.IncludeFile
307329
require $this->_filesystem->getDirectoryRead(DirectoryList::PUB)->getAbsolutePath('errors/404.php');
308330
return true;
309331
}
@@ -335,6 +357,7 @@ private function handleGenericReport(Bootstrap $bootstrap, \Exception $exception
335357
if (isset($params['SCRIPT_NAME'])) {
336358
$reportData['script_name'] = $params['SCRIPT_NAME'];
337359
}
360+
// phpcs:ignore Magento2.Security.IncludeFile
338361
require $this->_filesystem->getDirectoryRead(DirectoryList::PUB)->getAbsolutePath('errors/report.php');
339362
return true;
340363
}

lib/internal/Magento/Framework/App/Test/Unit/HttpTest.php

Lines changed: 126 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ protected function setUp()
9292
'pathInfoProcessor' => $pathInfoProcessorMock,
9393
'objectManager' => $objectManagerMock
9494
])
95-
->setMethods(['getFrontName'])
95+
->setMethods(['getFrontName', 'isHead'])
9696
->getMock();
9797
$this->areaListMock = $this->getMockBuilder(\Magento\Framework\App\AreaList::class)
9898
->disableOriginalConstructor()
@@ -135,12 +135,17 @@ private function setUpLaunch()
135135
{
136136
$frontName = 'frontName';
137137
$areaCode = 'areaCode';
138-
$this->requestMock->expects($this->once())->method('getFrontName')->will($this->returnValue($frontName));
138+
$this->requestMock->expects($this->once())
139+
->method('getFrontName')
140+
->willReturn($frontName);
139141
$this->areaListMock->expects($this->once())
140142
->method('getCodeByFrontName')
141-
->with($frontName)->will($this->returnValue($areaCode));
143+
->with($frontName)
144+
->willReturn($areaCode);
142145
$this->configLoaderMock->expects($this->once())
143-
->method('load')->with($areaCode)->will($this->returnValue([]));
146+
->method('load')
147+
->with($areaCode)
148+
->willReturn([]);
144149
$this->objectManagerMock->expects($this->once())->method('configure')->with([]);
145150
$this->objectManagerMock->expects($this->once())
146151
->method('get')
@@ -149,12 +154,15 @@ private function setUpLaunch()
149154
$this->frontControllerMock->expects($this->once())
150155
->method('dispatch')
151156
->with($this->requestMock)
152-
->will($this->returnValue($this->responseMock));
157+
->willReturn($this->responseMock);
153158
}
154159

155160
public function testLaunchSuccess()
156161
{
157162
$this->setUpLaunch();
163+
$this->requestMock->expects($this->once())
164+
->method('isHead')
165+
->willReturn(false);
158166
$this->eventManagerMock->expects($this->once())
159167
->method('dispatch')
160168
->with(
@@ -171,33 +179,101 @@ public function testLaunchSuccess()
171179
public function testLaunchException()
172180
{
173181
$this->setUpLaunch();
174-
$this->frontControllerMock->expects($this->once())->method('dispatch')->with($this->requestMock)->will(
175-
$this->returnCallback(
176-
function () {
177-
throw new \Exception('Message');
178-
}
179-
)
180-
);
182+
$this->frontControllerMock->expects($this->once())
183+
->method('dispatch')
184+
->with($this->requestMock)
185+
->willThrowException(
186+
new \Exception('Message')
187+
);
181188
$this->http->launch();
182189
}
183190

191+
/**
192+
* Test that HEAD requests lead to an empty body and a Content-Length header matching the original body size.
193+
* @dataProvider dataProviderForTestLaunchHeadRequest
194+
* @param string $body
195+
* @param int $expectedLength
196+
*/
197+
public function testLaunchHeadRequest($body, $expectedLength)
198+
{
199+
$this->setUpLaunch();
200+
$this->requestMock->expects($this->once())
201+
->method('isHead')
202+
->willReturn(true);
203+
$this->responseMock->expects($this->once())
204+
->method('getHttpResponseCode')
205+
->willReturn(200);
206+
$this->responseMock->expects($this->once())
207+
->method('getContent')
208+
->willReturn($body);
209+
$this->responseMock->expects($this->once())
210+
->method('clearBody')
211+
->willReturn($this->responseMock);
212+
$this->responseMock->expects($this->once())
213+
->method('setHeader')
214+
->with('Content-Length', $expectedLength)
215+
->willReturn($this->responseMock);
216+
$this->eventManagerMock->expects($this->once())
217+
->method('dispatch')
218+
->with(
219+
'controller_front_send_response_before',
220+
['request' => $this->requestMock, 'response' => $this->responseMock]
221+
);
222+
$this->assertSame($this->responseMock, $this->http->launch());
223+
}
224+
225+
/**
226+
* Different test content for responseMock with their expected lengths in bytes.
227+
* @return array
228+
*/
229+
public function dataProviderForTestLaunchHeadRequest(): array
230+
{
231+
return [
232+
[
233+
"<html><head></head><body>Test</body></html>", // Ascii text
234+
43 // Expected Content-Length
235+
],
236+
[
237+
"<html><head></head><body>部落格</body></html>", // Multi-byte characters
238+
48 // Expected Content-Length
239+
],
240+
[
241+
"<html><head></head><body>\0</body></html>", // Null byte
242+
40 // Expected Content-Length
243+
],
244+
[
245+
"<html><head></head>خرید<body></body></html>", // LTR text
246+
47 // Expected Content-Length
247+
]
248+
];
249+
}
250+
184251
public function testHandleDeveloperModeNotInstalled()
185252
{
186253
$dir = $this->getMockForAbstractClass(\Magento\Framework\Filesystem\Directory\ReadInterface::class);
187-
$dir->expects($this->once())->method('getAbsolutePath')->willReturn(__DIR__);
254+
$dir->expects($this->once())
255+
->method('getAbsolutePath')
256+
->willReturn(__DIR__);
188257
$this->filesystemMock->expects($this->once())
189258
->method('getDirectoryRead')
190259
->with(DirectoryList::ROOT)
191260
->willReturn($dir);
192-
$this->responseMock->expects($this->once())->method('setRedirect')->with('/_files/');
193-
$this->responseMock->expects($this->once())->method('sendHeaders');
261+
$this->responseMock->expects($this->once())
262+
->method('setRedirect')
263+
->with('/_files/');
264+
$this->responseMock->expects($this->once())
265+
->method('sendHeaders');
194266
$bootstrap = $this->getBootstrapNotInstalled();
195-
$bootstrap->expects($this->once())->method('getParams')->willReturn([
196-
'SCRIPT_NAME' => '/index.php',
197-
'DOCUMENT_ROOT' => __DIR__,
198-
'SCRIPT_FILENAME' => __DIR__ . '/index.php',
199-
SetupInfo::PARAM_NOT_INSTALLED_URL_PATH => '_files',
200-
]);
267+
$bootstrap->expects($this->once())
268+
->method('getParams')
269+
->willReturn(
270+
[
271+
'SCRIPT_NAME' => '/index.php',
272+
'DOCUMENT_ROOT' => __DIR__,
273+
'SCRIPT_FILENAME' => __DIR__ . '/index.php',
274+
SetupInfo::PARAM_NOT_INSTALLED_URL_PATH => '_files',
275+
]
276+
);
201277
$this->assertTrue($this->http->catchException($bootstrap, new \Exception('Test Message')));
202278
}
203279

@@ -206,24 +282,37 @@ public function testHandleDeveloperMode()
206282
$this->filesystemMock->expects($this->once())
207283
->method('getDirectoryRead')
208284
->will($this->throwException(new \Exception('strange error')));
209-
$this->responseMock->expects($this->once())->method('setHttpResponseCode')->with(500);
210-
$this->responseMock->expects($this->once())->method('setHeader')->with('Content-Type', 'text/plain');
285+
$this->responseMock->expects($this->once())
286+
->method('setHttpResponseCode')
287+
->with(500);
288+
$this->responseMock->expects($this->once())
289+
->method('setHeader')
290+
->with('Content-Type', 'text/plain');
211291
$constraint = new \PHPUnit\Framework\Constraint\StringStartsWith('1 exception(s):');
212-
$this->responseMock->expects($this->once())->method('setBody')->with($constraint);
213-
$this->responseMock->expects($this->once())->method('sendResponse');
292+
$this->responseMock->expects($this->once())
293+
->method('setBody')
294+
->with($constraint);
295+
$this->responseMock->expects($this->once())
296+
->method('sendResponse');
214297
$bootstrap = $this->getBootstrapNotInstalled();
215-
$bootstrap->expects($this->once())->method('getParams')->willReturn(
216-
['DOCUMENT_ROOT' => 'something', 'SCRIPT_FILENAME' => 'something/else']
217-
);
298+
$bootstrap->expects($this->once())
299+
->method('getParams')
300+
->willReturn(
301+
['DOCUMENT_ROOT' => 'something', 'SCRIPT_FILENAME' => 'something/else']
302+
);
218303
$this->assertTrue($this->http->catchException($bootstrap, new \Exception('Test')));
219304
}
220305

221306
public function testCatchExceptionSessionException()
222307
{
223-
$this->responseMock->expects($this->once())->method('setRedirect');
224-
$this->responseMock->expects($this->once())->method('sendHeaders');
308+
$this->responseMock->expects($this->once())
309+
->method('setRedirect');
310+
$this->responseMock->expects($this->once())
311+
->method('sendHeaders');
225312
$bootstrap = $this->createMock(\Magento\Framework\App\Bootstrap::class);
226-
$bootstrap->expects($this->once())->method('isDeveloperMode')->willReturn(false);
313+
$bootstrap->expects($this->once())
314+
->method('isDeveloperMode')
315+
->willReturn(false);
227316
$this->assertTrue($this->http->catchException(
228317
$bootstrap,
229318
new \Magento\Framework\Exception\SessionException(new \Magento\Framework\Phrase('Test'))
@@ -238,8 +327,12 @@ public function testCatchExceptionSessionException()
238327
private function getBootstrapNotInstalled()
239328
{
240329
$bootstrap = $this->createMock(\Magento\Framework\App\Bootstrap::class);
241-
$bootstrap->expects($this->once())->method('isDeveloperMode')->willReturn(true);
242-
$bootstrap->expects($this->once())->method('getErrorCode')->willReturn(Bootstrap::ERR_IS_INSTALLED);
330+
$bootstrap->expects($this->once())
331+
->method('isDeveloperMode')
332+
->willReturn(true);
333+
$bootstrap->expects($this->once())
334+
->method('getErrorCode')
335+
->willReturn(Bootstrap::ERR_IS_INSTALLED);
243336
return $bootstrap;
244337
}
245338
}

0 commit comments

Comments
 (0)