Skip to content

Commit aed3169

Browse files
authored
ENGCOM-4474: Fix for issue #21299. Change HEAD action mapping to GET action interface and add HEAD request handling #21378
2 parents 3e624c4 + 6d196b2 commit aed3169

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)