diff --git a/patches.json b/patches.json
index bec8c20..f81ca12 100644
--- a/patches.json
+++ b/patches.json
@@ -189,6 +189,18 @@
},
"Catalog Product collection method chaining is broken": {
"2.3.3": "MC-21820__fix_broken_method_chaining__2.3.3.patch"
+ },
+ "Reduce q-ty of error report files": {
+ "2.1.4 - 2.1.7": "MAGECLOUD-3392__reduce_q-ty_of_error_report_files__2.1.4.patch",
+ "2.1.8 - 2.1.16": "MAGECLOUD-3392__reduce_q-ty_of_error_report_files__2.1.8.patch",
+ "~2.1.17": "MAGECLOUD-3392__reduce_q-ty_of_error_report_files__2.1.17.patch",
+ "2.2.0 - 2.2.4": "MAGECLOUD-3392__reduce_q-ty_of_error_report_files__2.2.0.patch",
+ "2.2.5": "MAGECLOUD-3392__reduce_q-ty_of_error_report_files__2.2.5.patch",
+ "2.2.6 - 2.2.7": "MAGECLOUD-3392__reduce_q-ty_of_error_report_files__2.2.6.patch",
+ "~2.2.8": "MAGECLOUD-3392__reduce_q-ty_of_error_report_files__2.2.8.patch",
+ "2.3.0": "MAGECLOUD-3392__reduce_q-ty_of_error_report_files__2.3.0.patch",
+ "2.3.1": "MAGECLOUD-3392__reduce_q-ty_of_error_report_files__2.3.1.patch",
+ "2.3.2 - 2.3.3": "MAGECLOUD-3392__reduce_q-ty_of_error_report_files__2.3.2.patch"
}
},
"monolog/monolog": {
@@ -235,5 +247,10 @@
"Avoid group concat from source item indexer": {
">=1.0.3 <1.0.5": "MSI-GH-2515__eliminate_group_concat_from_source_item_indexer__grouped-product-indexer__1.0.3.patch"
}
+ },
+ "vertex/module-tax": {
+ "Fix wrong namespace": {
+ "3.2.0": "MAGECLOUD-4407__fix_namespace_vertex_tax__3.2.0.patch"
+ }
}
}
diff --git a/patches/MAGECLOUD-3392__reduce_q-ty_of_error_report_files__2.1.17.patch b/patches/MAGECLOUD-3392__reduce_q-ty_of_error_report_files__2.1.17.patch
new file mode 100644
index 0000000..a12bf65
--- /dev/null
+++ b/patches/MAGECLOUD-3392__reduce_q-ty_of_error_report_files__2.1.17.patch
@@ -0,0 +1,980 @@
+diff -Naur a/app/etc/di.xml b/app/etc/di.xml
+--- a/app/etc/di.xml
++++ b/app/etc/di.xml
+@@ -38,6 +38,7 @@
+
+
+
++
+
+
+ system/currency/installed
+diff -Naur a/vendor/magento/framework/App/ExceptionHandler.php b/vendor/magento/framework/App/ExceptionHandler.php
+--- /dev/null
++++ b/vendor/magento/framework/App/ExceptionHandler.php
+@@ -0,0 +1,278 @@
++encryptor = $encryptor;
++ $this->filesystem = $filesystem;
++ $this->logger = $logger;
++ }
++
++ /**
++ * Handles exception of HTTP web application
++ *
++ * @param Bootstrap $bootstrap
++ * @param \Exception $exception
++ * @param ResponseHttp $response
++ * @param RequestHttp $request
++ * @return bool
++ */
++ public function handle(
++ Bootstrap $bootstrap,
++ \Exception $exception,
++ ResponseHttp $response,
++ RequestHttp $request
++ ) {
++ $result = $this->handleDeveloperMode($bootstrap, $exception, $response)
++ || $this->handleBootstrapErrors($bootstrap, $exception, $response)
++ || $this->handleSessionException($exception, $response, $request)
++ || $this->handleInitException($exception)
++ || $this->handleGenericReport($bootstrap, $exception);
++ return $result;
++ }
++
++ /**
++ * Error handler for developer mode
++ *
++ * @param Bootstrap $bootstrap
++ * @param \Exception $exception
++ * @param ResponseHttp $response
++ * @return bool
++ */
++ private function handleDeveloperMode(
++ Bootstrap $bootstrap,
++ \Exception $exception,
++ ResponseHttp $response
++ ) {
++ if ($bootstrap->isDeveloperMode()) {
++ if (Bootstrap::ERR_IS_INSTALLED == $bootstrap->getErrorCode()) {
++ try {
++ $this->redirectToSetup($bootstrap, $exception, $response);
++ return true;
++ } catch (\Exception $e) {
++ $exception = $e;
++ }
++ }
++ $response->setHttpResponseCode(500);
++ $response->setHeader('Content-Type', 'text/plain');
++ $response->setBody($this->buildContentFromException($exception));
++ $response->sendResponse();
++ return true;
++ }
++ return false;
++ }
++
++ /**
++ * Build content based on an exception
++ *
++ * @param \Exception $exception
++ * @return string
++ */
++ private function buildContentFromException(\Exception $exception)
++ {
++ /** @var \Exception[] $exceptions */
++ $exceptions = [];
++
++ do {
++ $exceptions[] = $exception;
++ } while ($exception = $exception->getPrevious());
++
++ $buffer = sprintf("%d exception(s):\n", count($exceptions));
++
++ foreach ($exceptions as $index => $exception) {
++ $buffer .= sprintf(
++ "Exception #%d (%s): %s\n",
++ $index,
++ get_class($exception),
++ $exception->getMessage()
++ );
++ }
++
++ foreach ($exceptions as $index => $exception) {
++ $buffer .= sprintf(
++ "\nException #%d (%s): %s\n%s\n",
++ $index,
++ get_class($exception),
++ $exception->getMessage(),
++ Debug::trace(
++ $exception->getTrace(),
++ true,
++ true,
++ (bool)getenv('MAGE_DEBUG_SHOW_ARGS')
++ )
++ );
++ }
++
++ return $buffer;
++ }
++
++ /**
++ * Handler for bootstrap errors
++ *
++ * @param Bootstrap $bootstrap
++ * @param \Exception &$exception
++ * @param ResponseHttp $response
++ * @return bool
++ */
++ private function handleBootstrapErrors(
++ Bootstrap $bootstrap,
++ \Exception &$exception,
++ ResponseHttp $response
++ ) {
++ $bootstrapCode = $bootstrap->getErrorCode();
++ if (Bootstrap::ERR_MAINTENANCE == $bootstrapCode) {
++ require $this->filesystem
++ ->getDirectoryRead(DirectoryList::PUB)
++ ->getAbsolutePath('errors/503.php');
++ return true;
++ }
++ if (Bootstrap::ERR_IS_INSTALLED == $bootstrapCode) {
++ try {
++ $this->redirectToSetup($bootstrap, $exception, $response);
++ return true;
++ } catch (\Exception $e) {
++ $exception = $e;
++ }
++ }
++ return false;
++ }
++
++ /**
++ * Handler for session errors
++ *
++ * @param \Exception $exception
++ * @param ResponseHttp $response
++ * @param RequestHttp $request
++ * @return bool
++ */
++ private function handleSessionException(
++ \Exception $exception,
++ ResponseHttp $response,
++ RequestHttp $request
++ ) {
++ if ($exception instanceof SessionException) {
++ $response->setRedirect($request->getDistroBaseUrl());
++ $response->sendHeaders();
++ return true;
++ }
++ return false;
++ }
++
++ /**
++ * Handler for application initialization errors
++ *
++ * @param \Exception $exception
++ * @return bool
++ */
++ private function handleInitException(\Exception $exception)
++ {
++ if ($exception instanceof InitException) {
++ $this->logger->critical($exception);
++ require $this->filesystem
++ ->getDirectoryRead(DirectoryList::PUB)
++ ->getAbsolutePath('errors/404.php');
++ return true;
++ }
++ return false;
++ }
++
++ /**
++ * Handle for any other errors
++ *
++ * @param Bootstrap $bootstrap
++ * @param \Exception $exception
++ * @return bool
++ */
++ private function handleGenericReport(Bootstrap $bootstrap, \Exception $exception)
++ {
++ $reportData = [
++ $exception->getMessage(),
++ Debug::trace(
++ $exception->getTrace(),
++ true,
++ false,
++ (bool)getenv('MAGE_DEBUG_SHOW_ARGS')
++ )
++ ];
++ $params = $bootstrap->getParams();
++ if (isset($params['REQUEST_URI'])) {
++ $reportData['url'] = $params['REQUEST_URI'];
++ }
++ if (isset($params['SCRIPT_NAME'])) {
++ $reportData['script_name'] = $params['SCRIPT_NAME'];
++ }
++ $reportData['report_id'] = $this->encryptor->getHash(implode('', $reportData));
++ $this->logger->critical($exception, ['report_id' => $reportData['report_id']]);
++ require $this->filesystem
++ ->getDirectoryRead(DirectoryList::PUB)
++ ->getAbsolutePath('errors/report.php');
++ return true;
++ }
++ /**
++ * If not installed, try to redirect to installation wizard
++ *
++ * @param Bootstrap $bootstrap
++ * @param \Exception $exception
++ * @param ResponseHttp $response
++ * @return void
++ * @throws \Exception
++ */
++ private function redirectToSetup(Bootstrap $bootstrap, \Exception $exception, ResponseHttp $response)
++ {
++ $setupInfo = new SetupInfo($bootstrap->getParams());
++ $projectRoot = $this->filesystem->getDirectoryRead(DirectoryList::ROOT)->getAbsolutePath();
++ if ($setupInfo->isAvailable()) {
++ $response->setRedirect($setupInfo->getUrl());
++ $response->sendHeaders();
++ } else {
++ $newMessage = $exception->getMessage() . "\nNOTE: You cannot install Magento using the Setup Wizard "
++ . "because the Magento setup directory cannot be accessed. \n"
++ . 'You can install Magento using either the command line or you must restore access '
++ . 'to the following directory: ' . $setupInfo->getDir($projectRoot) . "\n";
++
++ throw new \Exception($newMessage, 0, $exception);
++ }
++ }
++}
+diff -Naur a/vendor/magento/framework/App/ExceptionHandlerInterface.php b/vendor/magento/framework/App/ExceptionHandlerInterface.php
+--- /dev/null
++++ b/vendor/magento/framework/App/ExceptionHandlerInterface.php
+@@ -0,0 +1,31 @@
++_objectManager = $objectManager;
+ $this->_eventManager = $eventManager;
+@@ -101,38 +97,23 @@ class Http implements \Magento\Framework\AppInterface
+ $this->_response = $response;
+ $this->_configLoader = $configLoader;
+ $this->_state = $state;
+- $this->_filesystem = $filesystem;
+ $this->registry = $registry;
+- }
+-
+- /**
+- * Add new dependency
+- *
+- * @return \Psr\Log\LoggerInterface
+- *
+- * @deprecated
+- */
+- private function getLogger()
+- {
+- if (!$this->logger instanceof \Psr\Log\LoggerInterface) {
+- $this->logger = \Magento\Framework\App\ObjectManager::getInstance()->get(\Psr\Log\LoggerInterface::class);
+- }
+- return $this->logger;
++ $this->exceptionHandler = $exceptionHandler ?: $this->_objectManager->get(ExceptionHandlerInterface::class);
+ }
+
+ /**
+ * Run application
+ *
+- * @throws \InvalidArgumentException
+ * @return ResponseInterface
++ * @throws \InvalidArgumentException|LocalizedException
+ */
+ public function launch()
+ {
+ $areaCode = $this->_areaList->getCodeByFrontName($this->_request->getFrontName());
+ $this->_state->setAreaCode($areaCode);
+ $this->_objectManager->configure($this->_configLoader->load($areaCode));
+- /** @var \Magento\Framework\App\FrontControllerInterface $frontController */
+- $frontController = $this->_objectManager->get(\Magento\Framework\App\FrontControllerInterface::class);
++ /** @var FrontControllerInterface $frontController */
++ $frontController = $this->_objectManager->get(FrontControllerInterface::class);
+ $result = $frontController->dispatch($this->_request);
+ // TODO: Temporary solution until all controllers return ResultInterface (MAGETWO-28359)
+ if ($result instanceof ResultInterface) {
+@@ -154,188 +135,6 @@ class Http implements \Magento\Framework\AppInterface
+ */
+ public function catchException(Bootstrap $bootstrap, \Exception $exception)
+ {
+- $result = $this->handleDeveloperMode($bootstrap, $exception)
+- || $this->handleBootstrapErrors($bootstrap, $exception)
+- || $this->handleSessionException($exception)
+- || $this->handleInitException($exception)
+- || $this->handleGenericReport($bootstrap, $exception);
+- return $result;
+- }
+-
+- /**
+- * Error handler for developer mode
+- *
+- * @param Bootstrap $bootstrap
+- * @param \Exception $exception
+- * @return bool
+- */
+- private function handleDeveloperMode(Bootstrap $bootstrap, \Exception $exception)
+- {
+- if ($bootstrap->isDeveloperMode()) {
+- if (Bootstrap::ERR_IS_INSTALLED == $bootstrap->getErrorCode()) {
+- try {
+- $this->redirectToSetup($bootstrap, $exception);
+- return true;
+- } catch (\Exception $e) {
+- $exception = $e;
+- }
+- }
+- $this->_response->setHttpResponseCode(500);
+- $this->_response->setHeader('Content-Type', 'text/plain');
+- $this->_response->setBody($this->buildContentFromException($exception));
+- $this->_response->sendResponse();
+- return true;
+- }
+- return false;
+- }
+-
+- /**
+- * Build content based on an exception
+- *
+- * @param \Exception $exception
+- * @return string
+- */
+- private function buildContentFromException(\Exception $exception)
+- {
+- /** @var \Exception[] $exceptions */
+- $exceptions = [];
+-
+- do {
+- $exceptions[] = $exception;
+- } while ($exception = $exception->getPrevious());
+-
+- $buffer = sprintf("%d exception(s):\n", count($exceptions));
+-
+- foreach ($exceptions as $index => $exception) {
+- $buffer .= sprintf("Exception #%d (%s): %s\n", $index, get_class($exception), $exception->getMessage());
+- }
+-
+- foreach ($exceptions as $index => $exception) {
+- $buffer .= sprintf(
+- "\nException #%d (%s): %s\n%s\n",
+- $index,
+- get_class($exception),
+- $exception->getMessage(),
+- Debug::trace(
+- $exception->getTrace(),
+- true,
+- true,
+- (bool)getenv('MAGE_DEBUG_SHOW_ARGS')
+- )
+- );
+- }
+-
+- return $buffer;
+- }
+-
+- /**
+- * If not installed, try to redirect to installation wizard
+- *
+- * @param Bootstrap $bootstrap
+- * @param \Exception $exception
+- * @return void
+- * @throws \Exception
+- */
+- private function redirectToSetup(Bootstrap $bootstrap, \Exception $exception)
+- {
+- $setupInfo = new SetupInfo($bootstrap->getParams());
+- $projectRoot = $this->_filesystem->getDirectoryRead(DirectoryList::ROOT)->getAbsolutePath();
+- if ($setupInfo->isAvailable()) {
+- $this->_response->setRedirect($setupInfo->getUrl());
+- $this->_response->sendHeaders();
+- } else {
+- $newMessage = $exception->getMessage() . "\nNOTE: You cannot install Magento using the Setup Wizard "
+- . "because the Magento setup directory cannot be accessed. \n"
+- . 'You can install Magento using either the command line or you must restore access '
+- . 'to the following directory: ' . $setupInfo->getDir($projectRoot) . "\n";
+-
+- throw new \Exception($newMessage, 0, $exception);
+- }
+- }
+-
+- /**
+- * Handler for bootstrap errors
+- *
+- * @param Bootstrap $bootstrap
+- * @param \Exception &$exception
+- * @return bool
+- */
+- private function handleBootstrapErrors(Bootstrap $bootstrap, \Exception &$exception)
+- {
+- $bootstrapCode = $bootstrap->getErrorCode();
+- if (Bootstrap::ERR_MAINTENANCE == $bootstrapCode) {
+- require $this->_filesystem->getDirectoryRead(DirectoryList::PUB)->getAbsolutePath('errors/503.php');
+- return true;
+- }
+- if (Bootstrap::ERR_IS_INSTALLED == $bootstrapCode) {
+- try {
+- $this->redirectToSetup($bootstrap, $exception);
+- return true;
+- } catch (\Exception $e) {
+- $exception = $e;
+- }
+- }
+- return false;
+- }
+-
+- /**
+- * Handler for session errors
+- *
+- * @param \Exception $exception
+- * @return bool
+- */
+- private function handleSessionException(\Exception $exception)
+- {
+- if ($exception instanceof \Magento\Framework\Exception\SessionException) {
+- $this->_response->setRedirect($this->_request->getDistroBaseUrl());
+- $this->_response->sendHeaders();
+- return true;
+- }
+- return false;
+- }
+-
+- /**
+- * Handler for application initialization errors
+- *
+- * @param \Exception $exception
+- * @return bool
+- */
+- private function handleInitException(\Exception $exception)
+- {
+- if ($exception instanceof \Magento\Framework\Exception\State\InitException) {
+- $this->getLogger()->critical($exception);
+- require $this->_filesystem->getDirectoryRead(DirectoryList::PUB)->getAbsolutePath('errors/404.php');
+- return true;
+- }
+- return false;
+- }
+-
+- /**
+- * Handle for any other errors
+- *
+- * @param Bootstrap $bootstrap
+- * @param \Exception $exception
+- * @return bool
+- */
+- private function handleGenericReport(Bootstrap $bootstrap, \Exception $exception)
+- {
+- $reportData = [
+- $exception->getMessage(),
+- Debug::trace(
+- $exception->getTrace(),
+- true,
+- true,
+- (bool)getenv('MAGE_DEBUG_SHOW_ARGS')
+- )
+- ];
+- $params = $bootstrap->getParams();
+- if (isset($params['REQUEST_URI'])) {
+- $reportData['url'] = $params['REQUEST_URI'];
+- }
+- if (isset($params['SCRIPT_NAME'])) {
+- $reportData['script_name'] = $params['SCRIPT_NAME'];
+- }
+- require $this->_filesystem->getDirectoryRead(DirectoryList::PUB)->getAbsolutePath('errors/report.php');
+- return true;
++ return $this->exceptionHandler->handle($bootstrap, $exception, $this->_response, $this->_request);
+ }
+ }
+diff -Naur a/vendor/magento/framework/Logger/Monolog.php b/vendor/magento/framework/Logger/Monolog.php
+--- a/vendor/magento/framework/Logger/Monolog.php
++++ b/vendor/magento/framework/Logger/Monolog.php
+@@ -20,7 +20,11 @@ class Monolog extends Logger
+ */
+ public function addRecord($level, $message, array $context = [])
+ {
++ if ($message instanceof \Exception && !isset($context['exception'])) {
++ $context['exception'] = $message;
++ }
+ $context['is_exception'] = $message instanceof \Exception;
++ $message = $message instanceof \Exception ? $message->getMessage() : $message;
+ return parent::addRecord($level, $message, $context);
+ }
+ }
+diff -Naur a/pub/errors/local.xml.sample b/pub/errors/local.xml.sample
+--- a/pub/errors/local.xml.sample
++++ b/pub/errors/local.xml.sample
+@@ -27,5 +27,22 @@
+ value "delete" is for cleaning
+ -->
+ leave
++
++ 0
+
+
+diff -Naur a/pub/errors/processor.php b/pub/errors/processor.php
+--- a/pub/errors/processor.php
++++ b/pub/errors/processor.php
+@@ -4,7 +4,10 @@
+ * See COPYING.txt for license details.
+ */
+ namespace Magento\Framework\Error;
+-use Magento\Framework\Filesystem\DriverInterface;
++
++use Magento\Framework\Escaper;
++use Magento\Framework\App\ObjectManager;
++use Magento\Framework\App\Response\Http;
+
+ /**
+ * Error processor
+@@ -15,6 +18,7 @@ class Processor
+ const MAGE_ERRORS_DESIGN_XML = 'design.xml';
+ const DEFAULT_SKIN = 'default';
+ const ERROR_DIR = 'pub/errors';
++ const NUMBER_SYMBOLS_IN_SUBDIR_NAME = 2;
+
+ /**
+ * Page title
+@@ -61,7 +65,7 @@ class Processor
+ /**
+ * Report ID
+ *
+- * @var int
++ * @var string
+ */
+ public $reportId;
+
+@@ -122,18 +126,25 @@ class Processor
+ /**
+ * Http response
+ *
+- * @var \Magento\Framework\App\Response\Http
++ * @var Http
+ */
+ protected $_response;
+
+ /**
+- * @param \Magento\Framework\App\Response\Http $response
++ * @var Escaper
++ */
++ private $escaper;
++
++ /**
++ * @param Http $response
++ * @param Escaper $escaper
+ */
+- public function __construct(\Magento\Framework\App\Response\Http $response)
++ public function __construct(Http $response, Escaper $escaper = null)
+ {
+ $this->_response = $response;
+ $this->_errorDir = __DIR__ . '/';
+ $this->_reportDir = dirname(dirname($this->_errorDir)) . '/var/report/';
++ $this->escaper = $escaper ?: ObjectManager::getInstance()->get(Escaper::class);
+
+ if (!empty($_SERVER['SCRIPT_NAME'])) {
+ if (in_array(basename($_SERVER['SCRIPT_NAME'], '.php'), ['404', '503', 'report'])) {
+@@ -143,11 +154,6 @@ class Processor
+ }
+ }
+
+- $reportId = (isset($_GET['id'])) ? (int)$_GET['id'] : null;
+- if ($reportId) {
+- $this->loadReport($reportId);
+- }
+-
+ $this->_indexDir = $this->_getIndexDir();
+ $this->_root = is_dir($this->_indexDir . 'app');
+
+@@ -155,6 +161,10 @@ class Processor
+ if (isset($_GET['skin'])) {
+ $this->_setSkin($_GET['skin']);
+ }
++
++ if (isset($_GET['id'])) {
++ $this->loadReport($_GET['id']);
++ }
+ }
+
+ /**
+@@ -341,6 +351,9 @@ class Processor
+ if ((string)$local->report->trash) {
+ $config->trash = $local->report->trash;
+ }
++ if ($local->report->dir_nesting_level) {
++ $config->dir_nesting_level = (int)$local->report->dir_nesting_level;
++ }
+ if ((string)$local->skin) {
+ $this->_setSkin((string)$local->skin, $config);
+ }
+@@ -435,7 +448,7 @@ class Processor
+ $this->reportData['url'] = $this->getHostUrl() . $reportData['url'];
+ }
+
+- if ($this->reportData['script_name']) {
++ if (isset($this->reportData['script_name'])) {
+ $this->_scriptName = $this->reportData['script_name'];
+ }
+ }
+@@ -446,18 +459,19 @@ class Processor
+ * @param array $reportData
+ * @return void
+ */
+- public function saveReport($reportData)
++ public function saveReport(array $reportData)
+ {
+- $this->reportData = $reportData;
+- $this->reportId = abs(intval(microtime(true) * rand(100, 1000)));
+- $this->_reportFile = $this->_reportDir . '/' . $this->reportId;
+- $this->_setReportData($reportData);
+-
+- if (!file_exists($this->_reportDir)) {
+- @mkdir($this->_reportDir, 0777, true);
++ $this->reportId = $reportData['report_id'];
++ $this->_reportFile = $this->getReportPath(
++ $this->getReportDirNestingLevel($this->reportId),
++ $this->reportId
++ );
++ $reportDirName = dirname($this->_reportFile);
++ if (!file_exists($reportDirName)) {
++ @mkdir($reportDirName, 0777, true);
+ }
+-
+- @file_put_contents($this->_reportFile, serialize($reportData));
++ $this->_setReportData($reportData);
++ @file_put_contents($this->_reportFile, serialize($reportData).PHP_EOL);
+
+ if (isset($reportData['skin']) && self::DEFAULT_SKIN != $reportData['skin']) {
+ $this->_setSkin($reportData['skin']);
+@@ -480,14 +494,108 @@ class Processor
+ */
+ public function loadReport($reportId)
+ {
+- $this->reportId = $reportId;
+- $this->_reportFile = $this->_reportDir . '/' . $reportId;
++ try {
++ if (!$this->isReportIdValid($reportId)) {
++ throw new \RuntimeException("Report Id is invalid");
++ }
++ $reportFile = $this->findReportFile($reportId);
++ if (!is_readable($reportFile)) {
++ throw new \RuntimeException("Report file cannot be read");
++ }
++ $this->reportId = $reportId;
++ $this->_reportFile = $reportFile;
++ $this->_setReportData(unserialize(file_get_contents($this->_reportFile)));
++ } catch (\RuntimeException $e) {
++ $this->redirectToBaseUrl();
++ }
++ }
++
++ /**
++ * Searches for the report file and returns the path to it
++ *
++ * @param string $reportId
++ * @return string
++ * @throws \RuntimeException
++ */
++ private function findReportFile($reportId)
++ {
++ $reportFile = $this->getReportPath(
++ $this->getReportDirNestingLevel($reportId),
++ $reportId
++ );
++ if (file_exists($reportFile)) {
++ return $reportFile;
++ }
++ $maxReportDirNestingLevel = $this->getMaxReportDirNestingLevel($reportId);
++ for ($i = 0; $i <= $maxReportDirNestingLevel; $i++) {
++ $reportFile = $this->getReportPath($i, $reportId);
++ if (file_exists($reportFile)) {
++ return $reportFile;
++ }
++ }
++ throw new \RuntimeException("Report file not found");
++ }
++ /**
++ * Redirect to a base url
++ * @return void
++ */
++ private function redirectToBaseUrl()
++ {
++ header("Location: " . $this->getBaseUrl());
++ die();
++ }
++ /**
++ * Checks report id
++ *
++ * @param string $reportId
++ * @return bool
++ */
++ private function isReportIdValid($reportId)
++ {
++ return (bool)preg_match('/[a-fA-F0-9]{64}/', $reportId);
++ }
++ /**
++ * Returns nesting Level for the report files
++ *
++ * @var $reportId
++ * @return int
++ */
++ private function getReportDirNestingLevel($reportId)
++ {
++ $envName = 'MAGE_ERROR_REPORT_DIR_NESTING_LEVEL';
++ $value = isset($_ENV[$envName]) ? $_ENV[$envName] : getenv($envName);
++ if (false === $value && property_exists($this->_config, 'dir_nesting_level')) {
++ $value = $this->_config->dir_nesting_level;
++ }
++ $value = (int)$value;
++ $maxValue = $this->getMaxReportDirNestingLevel($reportId);
++ return 0 < $value && $maxValue >= $value ? $value : 0;
++ }
++ /**
++ * Returns maximum nesting level directories of report files
++ *
++ * @param string $reportId
++ * @return integer
++ */
++ private function getMaxReportDirNestingLevel($reportId)
++ {
++ return (int)floor(strlen($reportId) / self::NUMBER_SYMBOLS_IN_SUBDIR_NAME);
++ }
+
+- if (!file_exists($this->_reportFile) || !is_readable($this->_reportFile)) {
+- header("Location: " . $this->getBaseUrl());
+- die();
++ /**
++ * Get path to reports
++ *
++ * @param integer $reportDirNestingLevel
++ * @param string $reportId
++ * @return string
++ */
++ private function getReportPath( $reportDirNestingLevel, $reportId)
++ {
++ $reportDirPath = $this->_reportDir;
++ for ($i = 0, $j = 0; $j < $reportDirNestingLevel; $i += 2, $j++) {
++ $reportDirPath .= $reportId[$i] . $reportId[$i + 1] . '/';
+ }
+- $this->_setReportData(unserialize(file_get_contents($this->_reportFile)));
++ return $reportDirPath . $reportId;
+ }
+
+ /**
+@@ -499,11 +607,16 @@ class Processor
+ {
+ $this->pageTitle = 'Error Submission Form';
+
+- $this->postData['firstName'] = (isset($_POST['firstname'])) ? trim(htmlspecialchars($_POST['firstname'])) : '';
+- $this->postData['lastName'] = (isset($_POST['lastname'])) ? trim(htmlspecialchars($_POST['lastname'])) : '';
+- $this->postData['email'] = (isset($_POST['email'])) ? trim(htmlspecialchars($_POST['email'])) : '';
+- $this->postData['telephone'] = (isset($_POST['telephone'])) ? trim(htmlspecialchars($_POST['telephone'])) : '';
+- $this->postData['comment'] = (isset($_POST['comment'])) ? trim(htmlspecialchars($_POST['comment'])) : '';
++ $this->postData['firstName'] = (isset($_POST['firstname']))
++ ? trim($this->escaper->escapeHtml($_POST['firstname'])) : '';
++ $this->postData['lastName'] = (isset($_POST['lastname']))
++ ? trim($this->escaper->escapeHtml($_POST['lastname'])) : '';
++ $this->postData['email'] = (isset($_POST['email']))
++ ? trim($this->escaper->escapeHtml($_POST['email'])) : '';
++ $this->postData['telephone'] = (isset($_POST['telephone']))
++ ? trim($this->escaper->escapeHtml($_POST['telephone'])) : '';
++ $this->postData['comment'] = (isset($_POST['comment']))
++ ? trim($this->escaper->escapeHtml($_POST['comment'])) : '';
+
+ if (isset($_POST['submit'])) {
+ if ($this->_validate()) {
diff --git a/patches/MAGECLOUD-3392__reduce_q-ty_of_error_report_files__2.1.4.patch b/patches/MAGECLOUD-3392__reduce_q-ty_of_error_report_files__2.1.4.patch
new file mode 100644
index 0000000..dd4b274
--- /dev/null
+++ b/patches/MAGECLOUD-3392__reduce_q-ty_of_error_report_files__2.1.4.patch
@@ -0,0 +1,971 @@
+diff -Naur a/app/etc/di.xml b/app/etc/di.xml
+--- a/app/etc/di.xml
++++ b/app/etc/di.xml
+@@ -38,6 +38,7 @@
+
+
+
++
+
+
+ system/currency/installed
+diff -Naur a/vendor/magento/framework/App/ExceptionHandler.php b/vendor/magento/framework/App/ExceptionHandler.php
+--- /dev/null
++++ b/vendor/magento/framework/App/ExceptionHandler.php
+@@ -0,0 +1,278 @@
++encryptor = $encryptor;
++ $this->filesystem = $filesystem;
++ $this->logger = $logger;
++ }
++
++ /**
++ * Handles exception of HTTP web application
++ *
++ * @param Bootstrap $bootstrap
++ * @param \Exception $exception
++ * @param ResponseHttp $response
++ * @param RequestHttp $request
++ * @return bool
++ */
++ public function handle(
++ Bootstrap $bootstrap,
++ \Exception $exception,
++ ResponseHttp $response,
++ RequestHttp $request
++ ) {
++ $result = $this->handleDeveloperMode($bootstrap, $exception, $response)
++ || $this->handleBootstrapErrors($bootstrap, $exception, $response)
++ || $this->handleSessionException($exception, $response, $request)
++ || $this->handleInitException($exception)
++ || $this->handleGenericReport($bootstrap, $exception);
++ return $result;
++ }
++
++ /**
++ * Error handler for developer mode
++ *
++ * @param Bootstrap $bootstrap
++ * @param \Exception $exception
++ * @param ResponseHttp $response
++ * @return bool
++ */
++ private function handleDeveloperMode(
++ Bootstrap $bootstrap,
++ \Exception $exception,
++ ResponseHttp $response
++ ) {
++ if ($bootstrap->isDeveloperMode()) {
++ if (Bootstrap::ERR_IS_INSTALLED == $bootstrap->getErrorCode()) {
++ try {
++ $this->redirectToSetup($bootstrap, $exception, $response);
++ return true;
++ } catch (\Exception $e) {
++ $exception = $e;
++ }
++ }
++ $response->setHttpResponseCode(500);
++ $response->setHeader('Content-Type', 'text/plain');
++ $response->setBody($this->buildContentFromException($exception));
++ $response->sendResponse();
++ return true;
++ }
++ return false;
++ }
++
++ /**
++ * Build content based on an exception
++ *
++ * @param \Exception $exception
++ * @return string
++ */
++ private function buildContentFromException(\Exception $exception)
++ {
++ /** @var \Exception[] $exceptions */
++ $exceptions = [];
++
++ do {
++ $exceptions[] = $exception;
++ } while ($exception = $exception->getPrevious());
++
++ $buffer = sprintf("%d exception(s):\n", count($exceptions));
++
++ foreach ($exceptions as $index => $exception) {
++ $buffer .= sprintf(
++ "Exception #%d (%s): %s\n",
++ $index,
++ get_class($exception),
++ $exception->getMessage()
++ );
++ }
++
++ foreach ($exceptions as $index => $exception) {
++ $buffer .= sprintf(
++ "\nException #%d (%s): %s\n%s\n",
++ $index,
++ get_class($exception),
++ $exception->getMessage(),
++ Debug::trace(
++ $exception->getTrace(),
++ true,
++ true,
++ (bool)getenv('MAGE_DEBUG_SHOW_ARGS')
++ )
++ );
++ }
++
++ return $buffer;
++ }
++
++ /**
++ * Handler for bootstrap errors
++ *
++ * @param Bootstrap $bootstrap
++ * @param \Exception &$exception
++ * @param ResponseHttp $response
++ * @return bool
++ */
++ private function handleBootstrapErrors(
++ Bootstrap $bootstrap,
++ \Exception &$exception,
++ ResponseHttp $response
++ ) {
++ $bootstrapCode = $bootstrap->getErrorCode();
++ if (Bootstrap::ERR_MAINTENANCE == $bootstrapCode) {
++ require $this->filesystem
++ ->getDirectoryRead(DirectoryList::PUB)
++ ->getAbsolutePath('errors/503.php');
++ return true;
++ }
++ if (Bootstrap::ERR_IS_INSTALLED == $bootstrapCode) {
++ try {
++ $this->redirectToSetup($bootstrap, $exception, $response);
++ return true;
++ } catch (\Exception $e) {
++ $exception = $e;
++ }
++ }
++ return false;
++ }
++
++ /**
++ * Handler for session errors
++ *
++ * @param \Exception $exception
++ * @param ResponseHttp $response
++ * @param RequestHttp $request
++ * @return bool
++ */
++ private function handleSessionException(
++ \Exception $exception,
++ ResponseHttp $response,
++ RequestHttp $request
++ ) {
++ if ($exception instanceof SessionException) {
++ $response->setRedirect($request->getDistroBaseUrl());
++ $response->sendHeaders();
++ return true;
++ }
++ return false;
++ }
++
++ /**
++ * Handler for application initialization errors
++ *
++ * @param \Exception $exception
++ * @return bool
++ */
++ private function handleInitException(\Exception $exception)
++ {
++ if ($exception instanceof InitException) {
++ $this->logger->critical($exception);
++ require $this->filesystem
++ ->getDirectoryRead(DirectoryList::PUB)
++ ->getAbsolutePath('errors/404.php');
++ return true;
++ }
++ return false;
++ }
++
++ /**
++ * Handle for any other errors
++ *
++ * @param Bootstrap $bootstrap
++ * @param \Exception $exception
++ * @return bool
++ */
++ private function handleGenericReport(Bootstrap $bootstrap, \Exception $exception)
++ {
++ $reportData = [
++ $exception->getMessage(),
++ Debug::trace(
++ $exception->getTrace(),
++ true,
++ false,
++ (bool)getenv('MAGE_DEBUG_SHOW_ARGS')
++ )
++ ];
++ $params = $bootstrap->getParams();
++ if (isset($params['REQUEST_URI'])) {
++ $reportData['url'] = $params['REQUEST_URI'];
++ }
++ if (isset($params['SCRIPT_NAME'])) {
++ $reportData['script_name'] = $params['SCRIPT_NAME'];
++ }
++ $reportData['report_id'] = $this->encryptor->getHash(implode('', $reportData));
++ $this->logger->critical($exception, ['report_id' => $reportData['report_id']]);
++ require $this->filesystem
++ ->getDirectoryRead(DirectoryList::PUB)
++ ->getAbsolutePath('errors/report.php');
++ return true;
++ }
++ /**
++ * If not installed, try to redirect to installation wizard
++ *
++ * @param Bootstrap $bootstrap
++ * @param \Exception $exception
++ * @param ResponseHttp $response
++ * @return void
++ * @throws \Exception
++ */
++ private function redirectToSetup(Bootstrap $bootstrap, \Exception $exception, ResponseHttp $response)
++ {
++ $setupInfo = new SetupInfo($bootstrap->getParams());
++ $projectRoot = $this->filesystem->getDirectoryRead(DirectoryList::ROOT)->getAbsolutePath();
++ if ($setupInfo->isAvailable()) {
++ $response->setRedirect($setupInfo->getUrl());
++ $response->sendHeaders();
++ } else {
++ $newMessage = $exception->getMessage() . "\nNOTE: You cannot install Magento using the Setup Wizard "
++ . "because the Magento setup directory cannot be accessed. \n"
++ . 'You can install Magento using either the command line or you must restore access '
++ . 'to the following directory: ' . $setupInfo->getDir($projectRoot) . "\n";
++
++ throw new \Exception($newMessage, 0, $exception);
++ }
++ }
++}
+diff -Naur a/vendor/magento/framework/App/ExceptionHandlerInterface.php b/vendor/magento/framework/App/ExceptionHandlerInterface.php
+--- /dev/null
++++ b/vendor/magento/framework/App/ExceptionHandlerInterface.php
+@@ -0,0 +1,31 @@
++_objectManager = $objectManager;
+ $this->_eventManager = $eventManager;
+@@ -100,38 +97,23 @@ class Http implements \Magento\Framework\AppInterface
+ $this->_response = $response;
+ $this->_configLoader = $configLoader;
+ $this->_state = $state;
+- $this->_filesystem = $filesystem;
+ $this->registry = $registry;
+- }
+-
+- /**
+- * Add new dependency
+- *
+- * @return \Psr\Log\LoggerInterface
+- *
+- * @deprecated
+- */
+- private function getLogger()
+- {
+- if (!$this->logger instanceof \Psr\Log\LoggerInterface) {
+- $this->logger = \Magento\Framework\App\ObjectManager::getInstance()->get(\Psr\Log\LoggerInterface::class);
+- }
+- return $this->logger;
++ $this->exceptionHandler = $exceptionHandler ?: $this->_objectManager->get(ExceptionHandlerInterface::class);
+ }
+
+ /**
+ * Run application
+ *
+- * @throws \InvalidArgumentException
+ * @return ResponseInterface
++ * @throws \InvalidArgumentException|LocalizedException
+ */
+ public function launch()
+ {
+ $areaCode = $this->_areaList->getCodeByFrontName($this->_request->getFrontName());
+ $this->_state->setAreaCode($areaCode);
+ $this->_objectManager->configure($this->_configLoader->load($areaCode));
+- /** @var \Magento\Framework\App\FrontControllerInterface $frontController */
+- $frontController = $this->_objectManager->get('Magento\Framework\App\FrontControllerInterface');
++ /** @var FrontControllerInterface $frontController */
++ $frontController = $this->_objectManager->get(FrontControllerInterface::class);
+ $result = $frontController->dispatch($this->_request);
+ // TODO: Temporary solution until all controllers return ResultInterface (MAGETWO-28359)
+ if ($result instanceof ResultInterface) {
+@@ -149,179 +131,10 @@ class Http implements \Magento\Framework\AppInterface
+ }
+
+ /**
+- * {@inheritdoc}
++ * @inheritdoc
+ */
+ public function catchException(Bootstrap $bootstrap, \Exception $exception)
+ {
+- $result = $this->handleDeveloperMode($bootstrap, $exception)
+- || $this->handleBootstrapErrors($bootstrap, $exception)
+- || $this->handleSessionException($exception)
+- || $this->handleInitException($exception)
+- || $this->handleGenericReport($bootstrap, $exception);
+- return $result;
+- }
+-
+- /**
+- * Error handler for developer mode
+- *
+- * @param Bootstrap $bootstrap
+- * @param \Exception $exception
+- * @return bool
+- */
+- private function handleDeveloperMode(Bootstrap $bootstrap, \Exception $exception)
+- {
+- if ($bootstrap->isDeveloperMode()) {
+- if (Bootstrap::ERR_IS_INSTALLED == $bootstrap->getErrorCode()) {
+- try {
+- $this->redirectToSetup($bootstrap, $exception);
+- return true;
+- } catch (\Exception $e) {
+- $exception = $e;
+- }
+- }
+- $this->_response->setHttpResponseCode(500);
+- $this->_response->setHeader('Content-Type', 'text/plain');
+- $this->_response->setBody($this->buildContentFromException($exception));
+- $this->_response->sendResponse();
+- return true;
+- }
+- return false;
+- }
+-
+- /**
+- * Build content based on an exception
+- *
+- * @param \Exception $exception
+- * @return string
+- */
+- private function buildContentFromException(\Exception $exception)
+- {
+- /** @var \Exception[] $exceptions */
+- $exceptions = [];
+- do {
+- $exceptions[] = $exception;
+- } while ($exception = $exception->getPrevious());
+-
+- $buffer = sprintf("%d exception(s):\n", count($exceptions));
+-
+- foreach ($exceptions as $index => $exception) {
+- $buffer .= sprintf("Exception #%d (%s): %s\n", $index, get_class($exception), $exception->getMessage());
+- }
+-
+- foreach ($exceptions as $index => $exception) {
+- $buffer .= sprintf(
+- "\nException #%d (%s): %s\n%s\n",
+- $index,
+- get_class($exception),
+- $exception->getMessage(),
+- $exception->getTraceAsString()
+- );
+- }
+-
+- return $buffer;
+- }
+-
+- /**
+- * If not installed, try to redirect to installation wizard
+- *
+- * @param Bootstrap $bootstrap
+- * @param \Exception $exception
+- * @return void
+- * @throws \Exception
+- */
+- private function redirectToSetup(Bootstrap $bootstrap, \Exception $exception)
+- {
+- $setupInfo = new SetupInfo($bootstrap->getParams());
+- $projectRoot = $this->_filesystem->getDirectoryRead(DirectoryList::ROOT)->getAbsolutePath();
+- if ($setupInfo->isAvailable()) {
+- $this->_response->setRedirect($setupInfo->getUrl());
+- $this->_response->sendHeaders();
+- } else {
+- $newMessage = $exception->getMessage() . "\nNOTE: You cannot install Magento using the Setup Wizard "
+- . "because the Magento setup directory cannot be accessed. \n"
+- . 'You can install Magento using either the command line or you must restore access '
+- . 'to the following directory: ' . $setupInfo->getDir($projectRoot) . "\n";
+- $newMessage .= 'If you are using the sample nginx configuration, please go to '
+- . $this->_request->getScheme(). '://' . $this->_request->getHttpHost() . $setupInfo->getUrl();
+- throw new \Exception($newMessage, 0, $exception);
+- }
+- }
+-
+- /**
+- * Handler for bootstrap errors
+- *
+- * @param Bootstrap $bootstrap
+- * @param \Exception &$exception
+- * @return bool
+- */
+- private function handleBootstrapErrors(Bootstrap $bootstrap, \Exception &$exception)
+- {
+- $bootstrapCode = $bootstrap->getErrorCode();
+- if (Bootstrap::ERR_MAINTENANCE == $bootstrapCode) {
+- require $this->_filesystem->getDirectoryRead(DirectoryList::PUB)->getAbsolutePath('errors/503.php');
+- return true;
+- }
+- if (Bootstrap::ERR_IS_INSTALLED == $bootstrapCode) {
+- try {
+- $this->redirectToSetup($bootstrap, $exception);
+- return true;
+- } catch (\Exception $e) {
+- $exception = $e;
+- }
+- }
+- return false;
+- }
+-
+- /**
+- * Handler for session errors
+- *
+- * @param \Exception $exception
+- * @return bool
+- */
+- private function handleSessionException(\Exception $exception)
+- {
+- if ($exception instanceof \Magento\Framework\Exception\SessionException) {
+- $this->_response->setRedirect($this->_request->getDistroBaseUrl());
+- $this->_response->sendHeaders();
+- return true;
+- }
+- return false;
+- }
+-
+- /**
+- * Handler for application initialization errors
+- *
+- * @param \Exception $exception
+- * @return bool
+- */
+- private function handleInitException(\Exception $exception)
+- {
+- if ($exception instanceof \Magento\Framework\Exception\State\InitException) {
+- $this->getLogger()->critical($exception);
+- require $this->_filesystem->getDirectoryRead(DirectoryList::PUB)->getAbsolutePath('errors/404.php');
+- return true;
+- }
+- return false;
+- }
+-
+- /**
+- * Handle for any other errors
+- *
+- * @param Bootstrap $bootstrap
+- * @param \Exception $exception
+- * @return bool
+- */
+- private function handleGenericReport(Bootstrap $bootstrap, \Exception $exception)
+- {
+- $reportData = [$exception->getMessage(), $exception->getTraceAsString()];
+- $params = $bootstrap->getParams();
+- if (isset($params['REQUEST_URI'])) {
+- $reportData['url'] = $params['REQUEST_URI'];
+- }
+- if (isset($params['SCRIPT_NAME'])) {
+- $reportData['script_name'] = $params['SCRIPT_NAME'];
+- }
+- require $this->_filesystem->getDirectoryRead(DirectoryList::PUB)->getAbsolutePath('errors/report.php');
+- return true;
++ return $this->exceptionHandler->handle($bootstrap, $exception, $this->_response, $this->_request);
+ }
+ }
+diff -Naur a/vendor/magento/framework/Logger/Monolog.php b/vendor/magento/framework/Logger/Monolog.php
+--- a/vendor/magento/framework/Logger/Monolog.php
++++ b/vendor/magento/framework/Logger/Monolog.php
+@@ -20,7 +20,11 @@ class Monolog extends Logger
+ */
+ public function addRecord($level, $message, array $context = [])
+ {
++ if ($message instanceof \Exception && !isset($context['exception'])) {
++ $context['exception'] = $message;
++ }
+ $context['is_exception'] = $message instanceof \Exception;
++ $message = $message instanceof \Exception ? $message->getMessage() : $message;
+ return parent::addRecord($level, $message, $context);
+ }
+ }
+diff -Naur a/pub/errors/local.xml.sample b/pub/errors/local.xml.sample
+--- a/pub/errors/local.xml.sample
++++ b/pub/errors/local.xml.sample
+@@ -27,5 +27,22 @@
+ value "delete" is for cleaning
+ -->
+ leave
++
++ 0
+
+
+diff -Naur a/pub/errors/processor.php b/pub/errors/processor.php
+--- a/pub/errors/processor.php
++++ b/pub/errors/processor.php
+@@ -4,7 +4,10 @@
+ * See COPYING.txt for license details.
+ */
+ namespace Magento\Framework\Error;
+-use Magento\Framework\Filesystem\DriverInterface;
++
++use Magento\Framework\Escaper;
++use Magento\Framework\App\ObjectManager;
++use Magento\Framework\App\Response\Http;
+
+ /**
+ * Error processor
+@@ -15,6 +18,7 @@ class Processor
+ const MAGE_ERRORS_DESIGN_XML = 'design.xml';
+ const DEFAULT_SKIN = 'default';
+ const ERROR_DIR = 'pub/errors';
++ const NUMBER_SYMBOLS_IN_SUBDIR_NAME = 2;
+
+ /**
+ * Page title
+@@ -61,7 +65,7 @@ class Processor
+ /**
+ * Report ID
+ *
+- * @var int
++ * @var string
+ */
+ public $reportId;
+
+@@ -122,18 +126,25 @@ class Processor
+ /**
+ * Http response
+ *
+- * @var \Magento\Framework\App\Response\Http
++ * @var Http
+ */
+ protected $_response;
+
+ /**
+- * @param \Magento\Framework\App\Response\Http $response
++ * @var Escaper
++ */
++ private $escaper;
++
++ /**
++ * @param Http $response
++ * @param Escaper $escaper
+ */
+- public function __construct(\Magento\Framework\App\Response\Http $response)
++ public function __construct(Http $response, Escaper $escaper = null)
+ {
+ $this->_response = $response;
+ $this->_errorDir = __DIR__ . '/';
+ $this->_reportDir = dirname(dirname($this->_errorDir)) . '/var/report/';
++ $this->escaper = $escaper ?: ObjectManager::getInstance()->get(Escaper::class);
+
+ if (!empty($_SERVER['SCRIPT_NAME'])) {
+ if (in_array(basename($_SERVER['SCRIPT_NAME'], '.php'), ['404', '503', 'report'])) {
+@@ -143,11 +154,6 @@ class Processor
+ }
+ }
+
+- $reportId = (isset($_GET['id'])) ? (int)$_GET['id'] : null;
+- if ($reportId) {
+- $this->loadReport($reportId);
+- }
+-
+ $this->_indexDir = $this->_getIndexDir();
+ $this->_root = is_dir($this->_indexDir . 'app');
+
+@@ -155,6 +161,10 @@ class Processor
+ if (isset($_GET['skin'])) {
+ $this->_setSkin($_GET['skin']);
+ }
++
++ if (isset($_GET['id'])) {
++ $this->loadReport($_GET['id']);
++ }
+ }
+
+ /**
+@@ -341,6 +351,9 @@ class Processor
+ if ((string)$local->report->trash) {
+ $config->trash = $local->report->trash;
+ }
++ if ($local->report->dir_nesting_level) {
++ $config->dir_nesting_level = (int)$local->report->dir_nesting_level;
++ }
+ if ((string)$local->skin) {
+ $this->_setSkin((string)$local->skin, $config);
+ }
+@@ -435,7 +448,7 @@ class Processor
+ $this->reportData['url'] = $this->getHostUrl() . $reportData['url'];
+ }
+
+- if ($this->reportData['script_name']) {
++ if (isset($this->reportData['script_name'])) {
+ $this->_scriptName = $this->reportData['script_name'];
+ }
+ }
+@@ -446,18 +459,19 @@ class Processor
+ * @param array $reportData
+ * @return void
+ */
+- public function saveReport($reportData)
++ public function saveReport(array $reportData)
+ {
+- $this->reportData = $reportData;
+- $this->reportId = abs(intval(microtime(true) * rand(100, 1000)));
+- $this->_reportFile = $this->_reportDir . '/' . $this->reportId;
+- $this->_setReportData($reportData);
+-
+- if (!file_exists($this->_reportDir)) {
+- @mkdir($this->_reportDir, 0777, true);
++ $this->reportId = $reportData['report_id'];
++ $this->_reportFile = $this->getReportPath(
++ $this->getReportDirNestingLevel($this->reportId),
++ $this->reportId
++ );
++ $reportDirName = dirname($this->_reportFile);
++ if (!file_exists($reportDirName)) {
++ @mkdir($reportDirName, 0777, true);
+ }
+-
+- @file_put_contents($this->_reportFile, serialize($reportData));
++ $this->_setReportData($reportData);
++ @file_put_contents($this->_reportFile, serialize($reportData).PHP_EOL);
+
+ if (isset($reportData['skin']) && self::DEFAULT_SKIN != $reportData['skin']) {
+ $this->_setSkin($reportData['skin']);
+@@ -480,14 +494,108 @@ class Processor
+ */
+ public function loadReport($reportId)
+ {
+- $this->reportId = $reportId;
+- $this->_reportFile = $this->_reportDir . '/' . $reportId;
++ try {
++ if (!$this->isReportIdValid($reportId)) {
++ throw new \RuntimeException("Report Id is invalid");
++ }
++ $reportFile = $this->findReportFile($reportId);
++ if (!is_readable($reportFile)) {
++ throw new \RuntimeException("Report file cannot be read");
++ }
++ $this->reportId = $reportId;
++ $this->_reportFile = $reportFile;
++ $this->_setReportData(unserialize(file_get_contents($this->_reportFile)));
++ } catch (\RuntimeException $e) {
++ $this->redirectToBaseUrl();
++ }
++ }
++
++ /**
++ * Searches for the report file and returns the path to it
++ *
++ * @param string $reportId
++ * @return string
++ * @throws \RuntimeException
++ */
++ private function findReportFile($reportId)
++ {
++ $reportFile = $this->getReportPath(
++ $this->getReportDirNestingLevel($reportId),
++ $reportId
++ );
++ if (file_exists($reportFile)) {
++ return $reportFile;
++ }
++ $maxReportDirNestingLevel = $this->getMaxReportDirNestingLevel($reportId);
++ for ($i = 0; $i <= $maxReportDirNestingLevel; $i++) {
++ $reportFile = $this->getReportPath($i, $reportId);
++ if (file_exists($reportFile)) {
++ return $reportFile;
++ }
++ }
++ throw new \RuntimeException("Report file not found");
++ }
++ /**
++ * Redirect to a base url
++ * @return void
++ */
++ private function redirectToBaseUrl()
++ {
++ header("Location: " . $this->getBaseUrl());
++ die();
++ }
++ /**
++ * Checks report id
++ *
++ * @param string $reportId
++ * @return bool
++ */
++ private function isReportIdValid($reportId)
++ {
++ return (bool)preg_match('/[a-fA-F0-9]{64}/', $reportId);
++ }
++ /**
++ * Returns nesting Level for the report files
++ *
++ * @var $reportId
++ * @return int
++ */
++ private function getReportDirNestingLevel($reportId)
++ {
++ $envName = 'MAGE_ERROR_REPORT_DIR_NESTING_LEVEL';
++ $value = isset($_ENV[$envName]) ? $_ENV[$envName] : getenv($envName);
++ if (false === $value && property_exists($this->_config, 'dir_nesting_level')) {
++ $value = $this->_config->dir_nesting_level;
++ }
++ $value = (int)$value;
++ $maxValue = $this->getMaxReportDirNestingLevel($reportId);
++ return 0 < $value && $maxValue >= $value ? $value : 0;
++ }
++ /**
++ * Returns maximum nesting level directories of report files
++ *
++ * @param string $reportId
++ * @return integer
++ */
++ private function getMaxReportDirNestingLevel($reportId)
++ {
++ return (int)floor(strlen($reportId) / self::NUMBER_SYMBOLS_IN_SUBDIR_NAME);
++ }
+
+- if (!file_exists($this->_reportFile) || !is_readable($this->_reportFile)) {
+- header("Location: " . $this->getBaseUrl());
+- die();
++ /**
++ * Get path to reports
++ *
++ * @param integer $reportDirNestingLevel
++ * @param string $reportId
++ * @return string
++ */
++ private function getReportPath( $reportDirNestingLevel, $reportId)
++ {
++ $reportDirPath = $this->_reportDir;
++ for ($i = 0, $j = 0; $j < $reportDirNestingLevel; $i += 2, $j++) {
++ $reportDirPath .= $reportId[$i] . $reportId[$i + 1] . '/';
+ }
+- $this->_setReportData(unserialize(file_get_contents($this->_reportFile)));
++ return $reportDirPath . $reportId;
+ }
+
+ /**
+@@ -499,11 +607,16 @@ class Processor
+ {
+ $this->pageTitle = 'Error Submission Form';
+
+- $this->postData['firstName'] = (isset($_POST['firstname'])) ? trim(htmlspecialchars($_POST['firstname'])) : '';
+- $this->postData['lastName'] = (isset($_POST['lastname'])) ? trim(htmlspecialchars($_POST['lastname'])) : '';
+- $this->postData['email'] = (isset($_POST['email'])) ? trim(htmlspecialchars($_POST['email'])) : '';
+- $this->postData['telephone'] = (isset($_POST['telephone'])) ? trim(htmlspecialchars($_POST['telephone'])) : '';
+- $this->postData['comment'] = (isset($_POST['comment'])) ? trim(htmlspecialchars($_POST['comment'])) : '';
++ $this->postData['firstName'] = (isset($_POST['firstname']))
++ ? trim($this->escaper->escapeHtml($_POST['firstname'])) : '';
++ $this->postData['lastName'] = (isset($_POST['lastname']))
++ ? trim($this->escaper->escapeHtml($_POST['lastname'])) : '';
++ $this->postData['email'] = (isset($_POST['email']))
++ ? trim($this->escaper->escapeHtml($_POST['email'])) : '';
++ $this->postData['telephone'] = (isset($_POST['telephone']))
++ ? trim($this->escaper->escapeHtml($_POST['telephone'])) : '';
++ $this->postData['comment'] = (isset($_POST['comment']))
++ ? trim($this->escaper->escapeHtml($_POST['comment'])) : '';
+
+ if (isset($_POST['submit'])) {
+ if ($this->_validate()) {
diff --git a/patches/MAGECLOUD-3392__reduce_q-ty_of_error_report_files__2.1.8.patch b/patches/MAGECLOUD-3392__reduce_q-ty_of_error_report_files__2.1.8.patch
new file mode 100644
index 0000000..eb244a8
--- /dev/null
+++ b/patches/MAGECLOUD-3392__reduce_q-ty_of_error_report_files__2.1.8.patch
@@ -0,0 +1,970 @@
+diff -Naur a/app/etc/di.xml b/app/etc/di.xml
+--- a/app/etc/di.xml
++++ b/app/etc/di.xml
+@@ -38,6 +38,7 @@
+
+
+
++
+
+
+ system/currency/installed
+diff -Naur a/vendor/magento/framework/App/ExceptionHandler.php b/vendor/magento/framework/App/ExceptionHandler.php
+--- /dev/null
++++ b/vendor/magento/framework/App/ExceptionHandler.php
+@@ -0,0 +1,278 @@
++encryptor = $encryptor;
++ $this->filesystem = $filesystem;
++ $this->logger = $logger;
++ }
++
++ /**
++ * Handles exception of HTTP web application
++ *
++ * @param Bootstrap $bootstrap
++ * @param \Exception $exception
++ * @param ResponseHttp $response
++ * @param RequestHttp $request
++ * @return bool
++ */
++ public function handle(
++ Bootstrap $bootstrap,
++ \Exception $exception,
++ ResponseHttp $response,
++ RequestHttp $request
++ ) {
++ $result = $this->handleDeveloperMode($bootstrap, $exception, $response)
++ || $this->handleBootstrapErrors($bootstrap, $exception, $response)
++ || $this->handleSessionException($exception, $response, $request)
++ || $this->handleInitException($exception)
++ || $this->handleGenericReport($bootstrap, $exception);
++ return $result;
++ }
++
++ /**
++ * Error handler for developer mode
++ *
++ * @param Bootstrap $bootstrap
++ * @param \Exception $exception
++ * @param ResponseHttp $response
++ * @return bool
++ */
++ private function handleDeveloperMode(
++ Bootstrap $bootstrap,
++ \Exception $exception,
++ ResponseHttp $response
++ ) {
++ if ($bootstrap->isDeveloperMode()) {
++ if (Bootstrap::ERR_IS_INSTALLED == $bootstrap->getErrorCode()) {
++ try {
++ $this->redirectToSetup($bootstrap, $exception, $response);
++ return true;
++ } catch (\Exception $e) {
++ $exception = $e;
++ }
++ }
++ $response->setHttpResponseCode(500);
++ $response->setHeader('Content-Type', 'text/plain');
++ $response->setBody($this->buildContentFromException($exception));
++ $response->sendResponse();
++ return true;
++ }
++ return false;
++ }
++
++ /**
++ * Build content based on an exception
++ *
++ * @param \Exception $exception
++ * @return string
++ */
++ private function buildContentFromException(\Exception $exception)
++ {
++ /** @var \Exception[] $exceptions */
++ $exceptions = [];
++
++ do {
++ $exceptions[] = $exception;
++ } while ($exception = $exception->getPrevious());
++
++ $buffer = sprintf("%d exception(s):\n", count($exceptions));
++
++ foreach ($exceptions as $index => $exception) {
++ $buffer .= sprintf(
++ "Exception #%d (%s): %s\n",
++ $index,
++ get_class($exception),
++ $exception->getMessage()
++ );
++ }
++
++ foreach ($exceptions as $index => $exception) {
++ $buffer .= sprintf(
++ "\nException #%d (%s): %s\n%s\n",
++ $index,
++ get_class($exception),
++ $exception->getMessage(),
++ Debug::trace(
++ $exception->getTrace(),
++ true,
++ true,
++ (bool)getenv('MAGE_DEBUG_SHOW_ARGS')
++ )
++ );
++ }
++
++ return $buffer;
++ }
++
++ /**
++ * Handler for bootstrap errors
++ *
++ * @param Bootstrap $bootstrap
++ * @param \Exception &$exception
++ * @param ResponseHttp $response
++ * @return bool
++ */
++ private function handleBootstrapErrors(
++ Bootstrap $bootstrap,
++ \Exception &$exception,
++ ResponseHttp $response
++ ) {
++ $bootstrapCode = $bootstrap->getErrorCode();
++ if (Bootstrap::ERR_MAINTENANCE == $bootstrapCode) {
++ require $this->filesystem
++ ->getDirectoryRead(DirectoryList::PUB)
++ ->getAbsolutePath('errors/503.php');
++ return true;
++ }
++ if (Bootstrap::ERR_IS_INSTALLED == $bootstrapCode) {
++ try {
++ $this->redirectToSetup($bootstrap, $exception, $response);
++ return true;
++ } catch (\Exception $e) {
++ $exception = $e;
++ }
++ }
++ return false;
++ }
++
++ /**
++ * Handler for session errors
++ *
++ * @param \Exception $exception
++ * @param ResponseHttp $response
++ * @param RequestHttp $request
++ * @return bool
++ */
++ private function handleSessionException(
++ \Exception $exception,
++ ResponseHttp $response,
++ RequestHttp $request
++ ) {
++ if ($exception instanceof SessionException) {
++ $response->setRedirect($request->getDistroBaseUrl());
++ $response->sendHeaders();
++ return true;
++ }
++ return false;
++ }
++
++ /**
++ * Handler for application initialization errors
++ *
++ * @param \Exception $exception
++ * @return bool
++ */
++ private function handleInitException(\Exception $exception)
++ {
++ if ($exception instanceof InitException) {
++ $this->logger->critical($exception);
++ require $this->filesystem
++ ->getDirectoryRead(DirectoryList::PUB)
++ ->getAbsolutePath('errors/404.php');
++ return true;
++ }
++ return false;
++ }
++
++ /**
++ * Handle for any other errors
++ *
++ * @param Bootstrap $bootstrap
++ * @param \Exception $exception
++ * @return bool
++ */
++ private function handleGenericReport(Bootstrap $bootstrap, \Exception $exception)
++ {
++ $reportData = [
++ $exception->getMessage(),
++ Debug::trace(
++ $exception->getTrace(),
++ true,
++ false,
++ (bool)getenv('MAGE_DEBUG_SHOW_ARGS')
++ )
++ ];
++ $params = $bootstrap->getParams();
++ if (isset($params['REQUEST_URI'])) {
++ $reportData['url'] = $params['REQUEST_URI'];
++ }
++ if (isset($params['SCRIPT_NAME'])) {
++ $reportData['script_name'] = $params['SCRIPT_NAME'];
++ }
++ $reportData['report_id'] = $this->encryptor->getHash(implode('', $reportData));
++ $this->logger->critical($exception, ['report_id' => $reportData['report_id']]);
++ require $this->filesystem
++ ->getDirectoryRead(DirectoryList::PUB)
++ ->getAbsolutePath('errors/report.php');
++ return true;
++ }
++ /**
++ * If not installed, try to redirect to installation wizard
++ *
++ * @param Bootstrap $bootstrap
++ * @param \Exception $exception
++ * @param ResponseHttp $response
++ * @return void
++ * @throws \Exception
++ */
++ private function redirectToSetup(Bootstrap $bootstrap, \Exception $exception, ResponseHttp $response)
++ {
++ $setupInfo = new SetupInfo($bootstrap->getParams());
++ $projectRoot = $this->filesystem->getDirectoryRead(DirectoryList::ROOT)->getAbsolutePath();
++ if ($setupInfo->isAvailable()) {
++ $response->setRedirect($setupInfo->getUrl());
++ $response->sendHeaders();
++ } else {
++ $newMessage = $exception->getMessage() . "\nNOTE: You cannot install Magento using the Setup Wizard "
++ . "because the Magento setup directory cannot be accessed. \n"
++ . 'You can install Magento using either the command line or you must restore access '
++ . 'to the following directory: ' . $setupInfo->getDir($projectRoot) . "\n";
++
++ throw new \Exception($newMessage, 0, $exception);
++ }
++ }
++}
+diff -Naur a/vendor/magento/framework/App/ExceptionHandlerInterface.php b/vendor/magento/framework/App/ExceptionHandlerInterface.php
+--- /dev/null
++++ b/vendor/magento/framework/App/ExceptionHandlerInterface.php
+@@ -0,0 +1,31 @@
++_objectManager = $objectManager;
+ $this->_eventManager = $eventManager;
+@@ -100,38 +97,23 @@ class Http implements \Magento\Framework\AppInterface
+ $this->_response = $response;
+ $this->_configLoader = $configLoader;
+ $this->_state = $state;
+- $this->_filesystem = $filesystem;
+ $this->registry = $registry;
+- }
+-
+- /**
+- * Add new dependency
+- *
+- * @return \Psr\Log\LoggerInterface
+- *
+- * @deprecated
+- */
+- private function getLogger()
+- {
+- if (!$this->logger instanceof \Psr\Log\LoggerInterface) {
+- $this->logger = \Magento\Framework\App\ObjectManager::getInstance()->get(\Psr\Log\LoggerInterface::class);
+- }
+- return $this->logger;
++ $this->exceptionHandler = $exceptionHandler ?: $this->_objectManager->get(ExceptionHandlerInterface::class);
+ }
+
+ /**
+ * Run application
+ *
+- * @throws \InvalidArgumentException
+ * @return ResponseInterface
++ * @throws \InvalidArgumentException|LocalizedException
+ */
+ public function launch()
+ {
+ $areaCode = $this->_areaList->getCodeByFrontName($this->_request->getFrontName());
+ $this->_state->setAreaCode($areaCode);
+ $this->_objectManager->configure($this->_configLoader->load($areaCode));
+- /** @var \Magento\Framework\App\FrontControllerInterface $frontController */
+- $frontController = $this->_objectManager->get('Magento\Framework\App\FrontControllerInterface');
++ /** @var FrontControllerInterface $frontController */
++ $frontController = $this->_objectManager->get(FrontControllerInterface::class);
+ $result = $frontController->dispatch($this->_request);
+ // TODO: Temporary solution until all controllers return ResultInterface (MAGETWO-28359)
+ if ($result instanceof ResultInterface) {
+@@ -149,178 +131,10 @@ class Http implements \Magento\Framework\AppInterface
+ }
+
+ /**
+- * {@inheritdoc}
++ * @inheritdoc
+ */
+ public function catchException(Bootstrap $bootstrap, \Exception $exception)
+ {
+- $result = $this->handleDeveloperMode($bootstrap, $exception)
+- || $this->handleBootstrapErrors($bootstrap, $exception)
+- || $this->handleSessionException($exception)
+- || $this->handleInitException($exception)
+- || $this->handleGenericReport($bootstrap, $exception);
+- return $result;
+- }
+-
+- /**
+- * Error handler for developer mode
+- *
+- * @param Bootstrap $bootstrap
+- * @param \Exception $exception
+- * @return bool
+- */
+- private function handleDeveloperMode(Bootstrap $bootstrap, \Exception $exception)
+- {
+- if ($bootstrap->isDeveloperMode()) {
+- if (Bootstrap::ERR_IS_INSTALLED == $bootstrap->getErrorCode()) {
+- try {
+- $this->redirectToSetup($bootstrap, $exception);
+- return true;
+- } catch (\Exception $e) {
+- $exception = $e;
+- }
+- }
+- $this->_response->setHttpResponseCode(500);
+- $this->_response->setHeader('Content-Type', 'text/plain');
+- $this->_response->setBody($this->buildContentFromException($exception));
+- $this->_response->sendResponse();
+- return true;
+- }
+- return false;
+- }
+-
+- /**
+- * Build content based on an exception
+- *
+- * @param \Exception $exception
+- * @return string
+- */
+- private function buildContentFromException(\Exception $exception)
+- {
+- /** @var \Exception[] $exceptions */
+- $exceptions = [];
+- do {
+- $exceptions[] = $exception;
+- } while ($exception = $exception->getPrevious());
+-
+- $buffer = sprintf("%d exception(s):\n", count($exceptions));
+-
+- foreach ($exceptions as $index => $exception) {
+- $buffer .= sprintf("Exception #%d (%s): %s\n", $index, get_class($exception), $exception->getMessage());
+- }
+-
+- foreach ($exceptions as $index => $exception) {
+- $buffer .= sprintf(
+- "\nException #%d (%s): %s\n%s\n",
+- $index,
+- get_class($exception),
+- $exception->getMessage(),
+- $exception->getTraceAsString()
+- );
+- }
+-
+- return $buffer;
+- }
+-
+- /**
+- * If not installed, try to redirect to installation wizard
+- *
+- * @param Bootstrap $bootstrap
+- * @param \Exception $exception
+- * @return void
+- * @throws \Exception
+- */
+- private function redirectToSetup(Bootstrap $bootstrap, \Exception $exception)
+- {
+- $setupInfo = new SetupInfo($bootstrap->getParams());
+- $projectRoot = $this->_filesystem->getDirectoryRead(DirectoryList::ROOT)->getAbsolutePath();
+- if ($setupInfo->isAvailable()) {
+- $this->_response->setRedirect($setupInfo->getUrl());
+- $this->_response->sendHeaders();
+- } else {
+- $newMessage = $exception->getMessage() . "\nNOTE: You cannot install Magento using the Setup Wizard "
+- . "because the Magento setup directory cannot be accessed. \n"
+- . 'You can install Magento using either the command line or you must restore access '
+- . 'to the following directory: ' . $setupInfo->getDir($projectRoot) . "\n";
+-
+- throw new \Exception($newMessage, 0, $exception);
+- }
+- }
+-
+- /**
+- * Handler for bootstrap errors
+- *
+- * @param Bootstrap $bootstrap
+- * @param \Exception &$exception
+- * @return bool
+- */
+- private function handleBootstrapErrors(Bootstrap $bootstrap, \Exception &$exception)
+- {
+- $bootstrapCode = $bootstrap->getErrorCode();
+- if (Bootstrap::ERR_MAINTENANCE == $bootstrapCode) {
+- require $this->_filesystem->getDirectoryRead(DirectoryList::PUB)->getAbsolutePath('errors/503.php');
+- return true;
+- }
+- if (Bootstrap::ERR_IS_INSTALLED == $bootstrapCode) {
+- try {
+- $this->redirectToSetup($bootstrap, $exception);
+- return true;
+- } catch (\Exception $e) {
+- $exception = $e;
+- }
+- }
+- return false;
+- }
+-
+- /**
+- * Handler for session errors
+- *
+- * @param \Exception $exception
+- * @return bool
+- */
+- private function handleSessionException(\Exception $exception)
+- {
+- if ($exception instanceof \Magento\Framework\Exception\SessionException) {
+- $this->_response->setRedirect($this->_request->getDistroBaseUrl());
+- $this->_response->sendHeaders();
+- return true;
+- }
+- return false;
+- }
+-
+- /**
+- * Handler for application initialization errors
+- *
+- * @param \Exception $exception
+- * @return bool
+- */
+- private function handleInitException(\Exception $exception)
+- {
+- if ($exception instanceof \Magento\Framework\Exception\State\InitException) {
+- $this->getLogger()->critical($exception);
+- require $this->_filesystem->getDirectoryRead(DirectoryList::PUB)->getAbsolutePath('errors/404.php');
+- return true;
+- }
+- return false;
+- }
+-
+- /**
+- * Handle for any other errors
+- *
+- * @param Bootstrap $bootstrap
+- * @param \Exception $exception
+- * @return bool
+- */
+- private function handleGenericReport(Bootstrap $bootstrap, \Exception $exception)
+- {
+- $reportData = [$exception->getMessage(), $exception->getTraceAsString()];
+- $params = $bootstrap->getParams();
+- if (isset($params['REQUEST_URI'])) {
+- $reportData['url'] = $params['REQUEST_URI'];
+- }
+- if (isset($params['SCRIPT_NAME'])) {
+- $reportData['script_name'] = $params['SCRIPT_NAME'];
+- }
+- require $this->_filesystem->getDirectoryRead(DirectoryList::PUB)->getAbsolutePath('errors/report.php');
+- return true;
++ return $this->exceptionHandler->handle($bootstrap, $exception, $this->_response, $this->_request);
+ }
+ }
+diff -Naur a/vendor/magento/framework/Logger/Monolog.php b/vendor/magento/framework/Logger/Monolog.php
+--- a/vendor/magento/framework/Logger/Monolog.php
++++ b/vendor/magento/framework/Logger/Monolog.php
+@@ -20,7 +20,11 @@ class Monolog extends Logger
+ */
+ public function addRecord($level, $message, array $context = [])
+ {
++ if ($message instanceof \Exception && !isset($context['exception'])) {
++ $context['exception'] = $message;
++ }
+ $context['is_exception'] = $message instanceof \Exception;
++ $message = $message instanceof \Exception ? $message->getMessage() : $message;
+ return parent::addRecord($level, $message, $context);
+ }
+ }
+diff -Naur a/pub/errors/local.xml.sample b/pub/errors/local.xml.sample
+--- a/pub/errors/local.xml.sample
++++ b/pub/errors/local.xml.sample
+@@ -27,5 +27,22 @@
+ value "delete" is for cleaning
+ -->
+ leave
++
++ 0
+
+
+diff -Naur a/pub/errors/processor.php b/pub/errors/processor.php
+--- a/pub/errors/processor.php
++++ b/pub/errors/processor.php
+@@ -4,7 +4,10 @@
+ * See COPYING.txt for license details.
+ */
+ namespace Magento\Framework\Error;
+-use Magento\Framework\Filesystem\DriverInterface;
++
++use Magento\Framework\Escaper;
++use Magento\Framework\App\ObjectManager;
++use Magento\Framework\App\Response\Http;
+
+ /**
+ * Error processor
+@@ -15,6 +18,7 @@ class Processor
+ const MAGE_ERRORS_DESIGN_XML = 'design.xml';
+ const DEFAULT_SKIN = 'default';
+ const ERROR_DIR = 'pub/errors';
++ const NUMBER_SYMBOLS_IN_SUBDIR_NAME = 2;
+
+ /**
+ * Page title
+@@ -61,7 +65,7 @@ class Processor
+ /**
+ * Report ID
+ *
+- * @var int
++ * @var string
+ */
+ public $reportId;
+
+@@ -122,18 +126,25 @@ class Processor
+ /**
+ * Http response
+ *
+- * @var \Magento\Framework\App\Response\Http
++ * @var Http
+ */
+ protected $_response;
+
+ /**
+- * @param \Magento\Framework\App\Response\Http $response
++ * @var Escaper
++ */
++ private $escaper;
++
++ /**
++ * @param Http $response
++ * @param Escaper $escaper
+ */
+- public function __construct(\Magento\Framework\App\Response\Http $response)
++ public function __construct(Http $response, Escaper $escaper = null)
+ {
+ $this->_response = $response;
+ $this->_errorDir = __DIR__ . '/';
+ $this->_reportDir = dirname(dirname($this->_errorDir)) . '/var/report/';
++ $this->escaper = $escaper ?: ObjectManager::getInstance()->get(Escaper::class);
+
+ if (!empty($_SERVER['SCRIPT_NAME'])) {
+ if (in_array(basename($_SERVER['SCRIPT_NAME'], '.php'), ['404', '503', 'report'])) {
+@@ -143,11 +154,6 @@ class Processor
+ }
+ }
+
+- $reportId = (isset($_GET['id'])) ? (int)$_GET['id'] : null;
+- if ($reportId) {
+- $this->loadReport($reportId);
+- }
+-
+ $this->_indexDir = $this->_getIndexDir();
+ $this->_root = is_dir($this->_indexDir . 'app');
+
+@@ -155,6 +161,10 @@ class Processor
+ if (isset($_GET['skin'])) {
+ $this->_setSkin($_GET['skin']);
+ }
++
++ if (isset($_GET['id'])) {
++ $this->loadReport($_GET['id']);
++ }
+ }
+
+ /**
+@@ -341,6 +351,9 @@ class Processor
+ if ((string)$local->report->trash) {
+ $config->trash = $local->report->trash;
+ }
++ if ($local->report->dir_nesting_level) {
++ $config->dir_nesting_level = (int)$local->report->dir_nesting_level;
++ }
+ if ((string)$local->skin) {
+ $this->_setSkin((string)$local->skin, $config);
+ }
+@@ -435,7 +448,7 @@ class Processor
+ $this->reportData['url'] = $this->getHostUrl() . $reportData['url'];
+ }
+
+- if ($this->reportData['script_name']) {
++ if (isset($this->reportData['script_name'])) {
+ $this->_scriptName = $this->reportData['script_name'];
+ }
+ }
+@@ -446,18 +459,19 @@ class Processor
+ * @param array $reportData
+ * @return void
+ */
+- public function saveReport($reportData)
++ public function saveReport(array $reportData)
+ {
+- $this->reportData = $reportData;
+- $this->reportId = abs(intval(microtime(true) * rand(100, 1000)));
+- $this->_reportFile = $this->_reportDir . '/' . $this->reportId;
+- $this->_setReportData($reportData);
+-
+- if (!file_exists($this->_reportDir)) {
+- @mkdir($this->_reportDir, 0777, true);
++ $this->reportId = $reportData['report_id'];
++ $this->_reportFile = $this->getReportPath(
++ $this->getReportDirNestingLevel($this->reportId),
++ $this->reportId
++ );
++ $reportDirName = dirname($this->_reportFile);
++ if (!file_exists($reportDirName)) {
++ @mkdir($reportDirName, 0777, true);
+ }
+-
+- @file_put_contents($this->_reportFile, serialize($reportData));
++ $this->_setReportData($reportData);
++ @file_put_contents($this->_reportFile, serialize($reportData).PHP_EOL);
+
+ if (isset($reportData['skin']) && self::DEFAULT_SKIN != $reportData['skin']) {
+ $this->_setSkin($reportData['skin']);
+@@ -480,14 +494,108 @@ class Processor
+ */
+ public function loadReport($reportId)
+ {
+- $this->reportId = $reportId;
+- $this->_reportFile = $this->_reportDir . '/' . $reportId;
++ try {
++ if (!$this->isReportIdValid($reportId)) {
++ throw new \RuntimeException("Report Id is invalid");
++ }
++ $reportFile = $this->findReportFile($reportId);
++ if (!is_readable($reportFile)) {
++ throw new \RuntimeException("Report file cannot be read");
++ }
++ $this->reportId = $reportId;
++ $this->_reportFile = $reportFile;
++ $this->_setReportData(unserialize(file_get_contents($this->_reportFile)));
++ } catch (\RuntimeException $e) {
++ $this->redirectToBaseUrl();
++ }
++ }
++
++ /**
++ * Searches for the report file and returns the path to it
++ *
++ * @param string $reportId
++ * @return string
++ * @throws \RuntimeException
++ */
++ private function findReportFile($reportId)
++ {
++ $reportFile = $this->getReportPath(
++ $this->getReportDirNestingLevel($reportId),
++ $reportId
++ );
++ if (file_exists($reportFile)) {
++ return $reportFile;
++ }
++ $maxReportDirNestingLevel = $this->getMaxReportDirNestingLevel($reportId);
++ for ($i = 0; $i <= $maxReportDirNestingLevel; $i++) {
++ $reportFile = $this->getReportPath($i, $reportId);
++ if (file_exists($reportFile)) {
++ return $reportFile;
++ }
++ }
++ throw new \RuntimeException("Report file not found");
++ }
++ /**
++ * Redirect to a base url
++ * @return void
++ */
++ private function redirectToBaseUrl()
++ {
++ header("Location: " . $this->getBaseUrl());
++ die();
++ }
++ /**
++ * Checks report id
++ *
++ * @param string $reportId
++ * @return bool
++ */
++ private function isReportIdValid($reportId)
++ {
++ return (bool)preg_match('/[a-fA-F0-9]{64}/', $reportId);
++ }
++ /**
++ * Returns nesting Level for the report files
++ *
++ * @var $reportId
++ * @return int
++ */
++ private function getReportDirNestingLevel($reportId)
++ {
++ $envName = 'MAGE_ERROR_REPORT_DIR_NESTING_LEVEL';
++ $value = isset($_ENV[$envName]) ? $_ENV[$envName] : getenv($envName);
++ if (false === $value && property_exists($this->_config, 'dir_nesting_level')) {
++ $value = $this->_config->dir_nesting_level;
++ }
++ $value = (int)$value;
++ $maxValue = $this->getMaxReportDirNestingLevel($reportId);
++ return 0 < $value && $maxValue >= $value ? $value : 0;
++ }
++ /**
++ * Returns maximum nesting level directories of report files
++ *
++ * @param string $reportId
++ * @return integer
++ */
++ private function getMaxReportDirNestingLevel($reportId)
++ {
++ return (int)floor(strlen($reportId) / self::NUMBER_SYMBOLS_IN_SUBDIR_NAME);
++ }
+
+- if (!file_exists($this->_reportFile) || !is_readable($this->_reportFile)) {
+- header("Location: " . $this->getBaseUrl());
+- die();
++ /**
++ * Get path to reports
++ *
++ * @param integer $reportDirNestingLevel
++ * @param string $reportId
++ * @return string
++ */
++ private function getReportPath( $reportDirNestingLevel, $reportId)
++ {
++ $reportDirPath = $this->_reportDir;
++ for ($i = 0, $j = 0; $j < $reportDirNestingLevel; $i += 2, $j++) {
++ $reportDirPath .= $reportId[$i] . $reportId[$i + 1] . '/';
+ }
+- $this->_setReportData(unserialize(file_get_contents($this->_reportFile)));
++ return $reportDirPath . $reportId;
+ }
+
+ /**
+@@ -499,11 +607,16 @@ class Processor
+ {
+ $this->pageTitle = 'Error Submission Form';
+
+- $this->postData['firstName'] = (isset($_POST['firstname'])) ? trim(htmlspecialchars($_POST['firstname'])) : '';
+- $this->postData['lastName'] = (isset($_POST['lastname'])) ? trim(htmlspecialchars($_POST['lastname'])) : '';
+- $this->postData['email'] = (isset($_POST['email'])) ? trim(htmlspecialchars($_POST['email'])) : '';
+- $this->postData['telephone'] = (isset($_POST['telephone'])) ? trim(htmlspecialchars($_POST['telephone'])) : '';
+- $this->postData['comment'] = (isset($_POST['comment'])) ? trim(htmlspecialchars($_POST['comment'])) : '';
++ $this->postData['firstName'] = (isset($_POST['firstname']))
++ ? trim($this->escaper->escapeHtml($_POST['firstname'])) : '';
++ $this->postData['lastName'] = (isset($_POST['lastname']))
++ ? trim($this->escaper->escapeHtml($_POST['lastname'])) : '';
++ $this->postData['email'] = (isset($_POST['email']))
++ ? trim($this->escaper->escapeHtml($_POST['email'])) : '';
++ $this->postData['telephone'] = (isset($_POST['telephone']))
++ ? trim($this->escaper->escapeHtml($_POST['telephone'])) : '';
++ $this->postData['comment'] = (isset($_POST['comment']))
++ ? trim($this->escaper->escapeHtml($_POST['comment'])) : '';
+
+ if (isset($_POST['submit'])) {
+ if ($this->_validate()) {
diff --git a/patches/MAGECLOUD-3392__reduce_q-ty_of_error_report_files__2.2.0.patch b/patches/MAGECLOUD-3392__reduce_q-ty_of_error_report_files__2.2.0.patch
new file mode 100644
index 0000000..8e61140
--- /dev/null
+++ b/patches/MAGECLOUD-3392__reduce_q-ty_of_error_report_files__2.2.0.patch
@@ -0,0 +1,996 @@
+diff -Naur a/app/etc/di.xml b/app/etc/di.xml
+--- a/app/etc/di.xml
++++ b/app/etc/di.xml
+@@ -43,6 +43,7 @@
+
+
+
++
+
+
+ system/currency/installed
+diff -Naur a/vendor/magento/framework/App/ExceptionHandler.php b/vendor/magento/framework/App/ExceptionHandler.php
+--- /dev/null
++++ b/vendor/magento/framework/App/ExceptionHandler.php
+@@ -0,0 +1,280 @@
++encryptor = $encryptor;
++ $this->filesystem = $filesystem;
++ $this->logger = $logger;
++ }
++
++ /**
++ * Handles exception of HTTP web application
++ *
++ * @param Bootstrap $bootstrap
++ * @param \Exception $exception
++ * @param ResponseHttp $response
++ * @param RequestHttp $request
++ * @return bool
++ */
++ public function handle(
++ Bootstrap $bootstrap,
++ \Exception $exception,
++ ResponseHttp $response,
++ RequestHttp $request
++ ): bool {
++ $result = $this->handleDeveloperMode($bootstrap, $exception, $response)
++ || $this->handleBootstrapErrors($bootstrap, $exception, $response)
++ || $this->handleSessionException($exception, $response, $request)
++ || $this->handleInitException($exception)
++ || $this->handleGenericReport($bootstrap, $exception);
++ return $result;
++ }
++
++ /**
++ * Error handler for developer mode
++ *
++ * @param Bootstrap $bootstrap
++ * @param \Exception $exception
++ * @param ResponseHttp $response
++ * @return bool
++ */
++ private function handleDeveloperMode(
++ Bootstrap $bootstrap,
++ \Exception $exception,
++ ResponseHttp $response
++ ): bool {
++ if ($bootstrap->isDeveloperMode()) {
++ if (Bootstrap::ERR_IS_INSTALLED == $bootstrap->getErrorCode()) {
++ try {
++ $this->redirectToSetup($bootstrap, $exception, $response);
++ return true;
++ } catch (\Exception $e) {
++ $exception = $e;
++ }
++ }
++ $response->setHttpResponseCode(500);
++ $response->setHeader('Content-Type', 'text/plain');
++ $response->setBody($this->buildContentFromException($exception));
++ $response->sendResponse();
++ return true;
++ }
++ return false;
++ }
++
++ /**
++ * Build content based on an exception
++ *
++ * @param \Exception $exception
++ * @return string
++ */
++ private function buildContentFromException(\Exception $exception) :string
++ {
++ /** @var \Exception[] $exceptions */
++ $exceptions = [];
++
++ do {
++ $exceptions[] = $exception;
++ } while ($exception = $exception->getPrevious());
++
++ $buffer = sprintf("%d exception(s):\n", count($exceptions));
++
++ foreach ($exceptions as $index => $exception) {
++ $buffer .= sprintf(
++ "Exception #%d (%s): %s\n",
++ $index,
++ get_class($exception),
++ $exception->getMessage()
++ );
++ }
++
++ foreach ($exceptions as $index => $exception) {
++ $buffer .= sprintf(
++ "\nException #%d (%s): %s\n%s\n",
++ $index,
++ get_class($exception),
++ $exception->getMessage(),
++ Debug::trace(
++ $exception->getTrace(),
++ true,
++ true,
++ (bool)getenv('MAGE_DEBUG_SHOW_ARGS')
++ )
++ );
++ }
++
++ return $buffer;
++ }
++
++ /**
++ * Handler for bootstrap errors
++ *
++ * @param Bootstrap $bootstrap
++ * @param \Exception &$exception
++ * @param ResponseHttp $response
++ * @return bool
++ */
++ private function handleBootstrapErrors(
++ Bootstrap $bootstrap,
++ \Exception &$exception,
++ ResponseHttp $response
++ ): bool {
++ $bootstrapCode = $bootstrap->getErrorCode();
++ if (Bootstrap::ERR_MAINTENANCE == $bootstrapCode) {
++ require $this->filesystem
++ ->getDirectoryRead(DirectoryList::PUB)
++ ->getAbsolutePath('errors/503.php');
++ return true;
++ }
++ if (Bootstrap::ERR_IS_INSTALLED == $bootstrapCode) {
++ try {
++ $this->redirectToSetup($bootstrap, $exception, $response);
++ return true;
++ } catch (\Exception $e) {
++ $exception = $e;
++ }
++ }
++ return false;
++ }
++
++ /**
++ * Handler for session errors
++ *
++ * @param \Exception $exception
++ * @param ResponseHttp $response
++ * @param RequestHttp $request
++ * @return bool
++ */
++ private function handleSessionException(
++ \Exception $exception,
++ ResponseHttp $response,
++ RequestHttp $request
++ ): bool {
++ if ($exception instanceof SessionException) {
++ $response->setRedirect($request->getDistroBaseUrl());
++ $response->sendHeaders();
++ return true;
++ }
++ return false;
++ }
++
++ /**
++ * Handler for application initialization errors
++ *
++ * @param \Exception $exception
++ * @return bool
++ */
++ private function handleInitException(\Exception $exception) : bool
++ {
++ if ($exception instanceof InitException) {
++ $this->logger->critical($exception);
++ require $this->filesystem
++ ->getDirectoryRead(DirectoryList::PUB)
++ ->getAbsolutePath('errors/404.php');
++ return true;
++ }
++ return false;
++ }
++
++ /**
++ * Handle for any other errors
++ *
++ * @param Bootstrap $bootstrap
++ * @param \Exception $exception
++ * @return bool
++ */
++ private function handleGenericReport(Bootstrap $bootstrap, \Exception $exception): bool
++ {
++ $reportData = [
++ $exception->getMessage(),
++ Debug::trace(
++ $exception->getTrace(),
++ true,
++ false,
++ (bool)getenv('MAGE_DEBUG_SHOW_ARGS')
++ )
++ ];
++ $params = $bootstrap->getParams();
++ if (isset($params['REQUEST_URI'])) {
++ $reportData['url'] = $params['REQUEST_URI'];
++ }
++ if (isset($params['SCRIPT_NAME'])) {
++ $reportData['script_name'] = $params['SCRIPT_NAME'];
++ }
++ $reportData['report_id'] = $this->encryptor->getHash(implode('', $reportData));
++ $this->logger->critical($exception, ['report_id' => $reportData['report_id']]);
++ require $this->filesystem
++ ->getDirectoryRead(DirectoryList::PUB)
++ ->getAbsolutePath('errors/report.php');
++ return true;
++ }
++ /**
++ * If not installed, try to redirect to installation wizard
++ *
++ * @param Bootstrap $bootstrap
++ * @param \Exception $exception
++ * @param ResponseHttp $response
++ * @return void
++ * @throws \Exception
++ */
++ private function redirectToSetup(Bootstrap $bootstrap, \Exception $exception, ResponseHttp $response)
++ {
++ $setupInfo = new SetupInfo($bootstrap->getParams());
++ $projectRoot = $this->filesystem->getDirectoryRead(DirectoryList::ROOT)->getAbsolutePath();
++ if ($setupInfo->isAvailable()) {
++ $response->setRedirect($setupInfo->getUrl());
++ $response->sendHeaders();
++ } else {
++ $newMessage = $exception->getMessage() . "\nNOTE: You cannot install Magento using the Setup Wizard "
++ . "because the Magento setup directory cannot be accessed. \n"
++ . 'You can install Magento using either the command line or you must restore access '
++ . 'to the following directory: ' . $setupInfo->getDir($projectRoot) . "\n";
++
++ throw new \Exception($newMessage, 0, $exception);
++ }
++ }
++}
+diff -Naur a/vendor/magento/framework/App/ExceptionHandlerInterface.php b/vendor/magento/framework/App/ExceptionHandlerInterface.php
+--- /dev/null
++++ b/vendor/magento/framework/App/ExceptionHandlerInterface.php
+@@ -0,0 +1,31 @@
++_objectManager = $objectManager;
+ $this->_eventManager = $eventManager;
+@@ -100,30 +97,15 @@ class Http implements \Magento\Framework\AppInterface
+ $this->_response = $response;
+ $this->_configLoader = $configLoader;
+ $this->_state = $state;
+- $this->_filesystem = $filesystem;
+ $this->registry = $registry;
+- }
+-
+- /**
+- * Add new dependency
+- *
+- * @return \Psr\Log\LoggerInterface
+- *
+- * @deprecated 100.1.0
+- */
+- private function getLogger()
+- {
+- if (!$this->logger instanceof \Psr\Log\LoggerInterface) {
+- $this->logger = \Magento\Framework\App\ObjectManager::getInstance()->get(\Psr\Log\LoggerInterface::class);
+- }
+- return $this->logger;
++ $this->exceptionHandler = $exceptionHandler ?: $this->_objectManager->get(ExceptionHandlerInterface::class);
+ }
+
+ /**
+ * Run application
+ *
+- * @throws \InvalidArgumentException
+ * @return ResponseInterface
++ * @throws \InvalidArgumentException|LocalizedException
+ */
+ public function launch()
+ {
+@@ -149,178 +131,10 @@ class Http implements \Magento\Framework\AppInterface
+ }
+
+ /**
+- * {@inheritdoc}
++ * @inheritdoc
+ */
+- public function catchException(Bootstrap $bootstrap, \Exception $exception)
++ public function catchException(Bootstrap $bootstrap, \Exception $exception): bool
+ {
+- $result = $this->handleDeveloperMode($bootstrap, $exception)
+- || $this->handleBootstrapErrors($bootstrap, $exception)
+- || $this->handleSessionException($exception)
+- || $this->handleInitException($exception)
+- || $this->handleGenericReport($bootstrap, $exception);
+- return $result;
+- }
+-
+- /**
+- * Error handler for developer mode
+- *
+- * @param Bootstrap $bootstrap
+- * @param \Exception $exception
+- * @return bool
+- */
+- private function handleDeveloperMode(Bootstrap $bootstrap, \Exception $exception)
+- {
+- if ($bootstrap->isDeveloperMode()) {
+- if (Bootstrap::ERR_IS_INSTALLED == $bootstrap->getErrorCode()) {
+- try {
+- $this->redirectToSetup($bootstrap, $exception);
+- return true;
+- } catch (\Exception $e) {
+- $exception = $e;
+- }
+- }
+- $this->_response->setHttpResponseCode(500);
+- $this->_response->setHeader('Content-Type', 'text/plain');
+- $this->_response->setBody($this->buildContentFromException($exception));
+- $this->_response->sendResponse();
+- return true;
+- }
+- return false;
+- }
+-
+- /**
+- * Build content based on an exception
+- *
+- * @param \Exception $exception
+- * @return string
+- */
+- private function buildContentFromException(\Exception $exception)
+- {
+- /** @var \Exception[] $exceptions */
+- $exceptions = [];
+- do {
+- $exceptions[] = $exception;
+- } while ($exception = $exception->getPrevious());
+-
+- $buffer = sprintf("%d exception(s):\n", count($exceptions));
+-
+- foreach ($exceptions as $index => $exception) {
+- $buffer .= sprintf("Exception #%d (%s): %s\n", $index, get_class($exception), $exception->getMessage());
+- }
+-
+- foreach ($exceptions as $index => $exception) {
+- $buffer .= sprintf(
+- "\nException #%d (%s): %s\n%s\n",
+- $index,
+- get_class($exception),
+- $exception->getMessage(),
+- $exception->getTraceAsString()
+- );
+- }
+-
+- return $buffer;
+- }
+-
+- /**
+- * If not installed, try to redirect to installation wizard
+- *
+- * @param Bootstrap $bootstrap
+- * @param \Exception $exception
+- * @return void
+- * @throws \Exception
+- */
+- private function redirectToSetup(Bootstrap $bootstrap, \Exception $exception)
+- {
+- $setupInfo = new SetupInfo($bootstrap->getParams());
+- $projectRoot = $this->_filesystem->getDirectoryRead(DirectoryList::ROOT)->getAbsolutePath();
+- if ($setupInfo->isAvailable()) {
+- $this->_response->setRedirect($setupInfo->getUrl());
+- $this->_response->sendHeaders();
+- } else {
+- $newMessage = $exception->getMessage() . "\nNOTE: You cannot install Magento using the Setup Wizard "
+- . "because the Magento setup directory cannot be accessed. \n"
+- . 'You can install Magento using either the command line or you must restore access '
+- . 'to the following directory: ' . $setupInfo->getDir($projectRoot) . "\n";
+-
+- throw new \Exception($newMessage, 0, $exception);
+- }
+- }
+-
+- /**
+- * Handler for bootstrap errors
+- *
+- * @param Bootstrap $bootstrap
+- * @param \Exception &$exception
+- * @return bool
+- */
+- private function handleBootstrapErrors(Bootstrap $bootstrap, \Exception &$exception)
+- {
+- $bootstrapCode = $bootstrap->getErrorCode();
+- if (Bootstrap::ERR_MAINTENANCE == $bootstrapCode) {
+- require $this->_filesystem->getDirectoryRead(DirectoryList::PUB)->getAbsolutePath('errors/503.php');
+- return true;
+- }
+- if (Bootstrap::ERR_IS_INSTALLED == $bootstrapCode) {
+- try {
+- $this->redirectToSetup($bootstrap, $exception);
+- return true;
+- } catch (\Exception $e) {
+- $exception = $e;
+- }
+- }
+- return false;
+- }
+-
+- /**
+- * Handler for session errors
+- *
+- * @param \Exception $exception
+- * @return bool
+- */
+- private function handleSessionException(\Exception $exception)
+- {
+- if ($exception instanceof \Magento\Framework\Exception\SessionException) {
+- $this->_response->setRedirect($this->_request->getDistroBaseUrl());
+- $this->_response->sendHeaders();
+- return true;
+- }
+- return false;
+- }
+-
+- /**
+- * Handler for application initialization errors
+- *
+- * @param \Exception $exception
+- * @return bool
+- */
+- private function handleInitException(\Exception $exception)
+- {
+- if ($exception instanceof \Magento\Framework\Exception\State\InitException) {
+- $this->getLogger()->critical($exception);
+- require $this->_filesystem->getDirectoryRead(DirectoryList::PUB)->getAbsolutePath('errors/404.php');
+- return true;
+- }
+- return false;
+- }
+-
+- /**
+- * Handle for any other errors
+- *
+- * @param Bootstrap $bootstrap
+- * @param \Exception $exception
+- * @return bool
+- */
+- private function handleGenericReport(Bootstrap $bootstrap, \Exception $exception)
+- {
+- $reportData = [$exception->getMessage(), $exception->getTraceAsString()];
+- $params = $bootstrap->getParams();
+- if (isset($params['REQUEST_URI'])) {
+- $reportData['url'] = $params['REQUEST_URI'];
+- }
+- if (isset($params['SCRIPT_NAME'])) {
+- $reportData['script_name'] = $params['SCRIPT_NAME'];
+- }
+- require $this->_filesystem->getDirectoryRead(DirectoryList::PUB)->getAbsolutePath('errors/report.php');
+- return true;
++ return $this->exceptionHandler->handle($bootstrap, $exception, $this->_response, $this->_request);
+ }
+ }
+diff -Naur a/pub/errors/local.xml.sample b/pub/errors/local.xml.sample
+--- a/pub/errors/local.xml.sample
++++ b/pub/errors/local.xml.sample
+@@ -27,5 +27,22 @@
+ value "delete" is for cleaning
+ -->
+ leave
++
++ 0
+
+
+diff -Naur a/pub/errors/processor.php b/pub/errors/processor.php
+--- a/pub/errors/processor.php
++++ b/pub/errors/processor.php
+@@ -6,11 +6,15 @@
+ namespace Magento\Framework\Error;
+
+ use Magento\Framework\Serialize\Serializer\Json;
++use Magento\Framework\Escaper;
++use Magento\Framework\App\ObjectManager;
++use Magento\Framework\App\Response\Http;
+
+ /**
+ * Error processor
+ *
+ * @SuppressWarnings(PHPMD.TooManyFields)
++ * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
+ */
+ class Processor
+ {
+@@ -18,6 +22,7 @@ class Processor
+ const MAGE_ERRORS_DESIGN_XML = 'design.xml';
+ const DEFAULT_SKIN = 'default';
+ const ERROR_DIR = 'pub/errors';
++ const NUMBER_SYMBOLS_IN_SUBDIR_NAME = 2;
+
+ /**
+ * Page title
+@@ -64,7 +69,7 @@ class Processor
+ /**
+ * Report ID
+ *
+- * @var int
++ * @var string
+ */
+ public $reportId;
+
+@@ -125,7 +130,7 @@ class Processor
+ /**
+ * Http response
+ *
+- * @var \Magento\Framework\App\Response\Http
++ * @var Http
+ */
+ protected $_response;
+
+@@ -137,15 +142,25 @@ class Processor
+ private $serializer;
+
+ /**
+- * @param \Magento\Framework\App\Response\Http $response
++ * @var Escaper
++ */
++ private $escaper;
++
++ /**
++ * @param Http $response
+ * @param Json $serializer
++ * @param Escaper $escaper
+ */
+- public function __construct(\Magento\Framework\App\Response\Http $response, Json $serializer = null)
+- {
++ public function __construct(
++ Http $response,
++ Json $serializer = null,
++ Escaper $escaper = null
++ ) {
+ $this->_response = $response;
+ $this->_errorDir = __DIR__ . '/';
+ $this->_reportDir = dirname(dirname($this->_errorDir)) . '/var/report/';
+- $this->serializer = $serializer ?: \Magento\Framework\App\ObjectManager::getInstance()->get(Json::class);
++ $this->serializer = $serializer ?: ObjectManager::getInstance()->get(Json::class);
++ $this->escaper = $escaper ?: ObjectManager::getInstance()->get(Escaper::class);
+
+ if (!empty($_SERVER['SCRIPT_NAME'])) {
+ if (in_array(basename($_SERVER['SCRIPT_NAME'], '.php'), ['404', '503', 'report'])) {
+@@ -155,11 +170,6 @@ class Processor
+ }
+ }
+
+- $reportId = (isset($_GET['id'])) ? (int)$_GET['id'] : null;
+- if ($reportId) {
+- $this->loadReport($reportId);
+- }
+-
+ $this->_indexDir = $this->_getIndexDir();
+ $this->_root = is_dir($this->_indexDir . 'app');
+
+@@ -167,6 +177,10 @@ class Processor
+ if (isset($_GET['skin'])) {
+ $this->_setSkin($_GET['skin']);
+ }
++
++ if (isset($_GET['id'])) {
++ $this->loadReport($_GET['id']);
++ }
+ }
+
+ /**
+@@ -268,10 +282,11 @@ class Processor
+ $isSecure = (!empty($_SERVER['HTTPS'])) && ($_SERVER['HTTPS'] != 'off');
+ $url = ($isSecure ? 'https://' : 'http://') . $host;
+
+- if (!empty($_SERVER['SERVER_PORT']) && !in_array($_SERVER['SERVER_PORT'], [80, 433])
++ $port = explode(':', $host);
++ if (isset($port[1]) && !in_array($port[1], [80, 443])
+ && !preg_match('/.*?\:[0-9]+$/', $url)
+ ) {
+- $url .= ':' . $_SERVER['SERVER_PORT'];
++ $url .= ':' . $port[1];
+ }
+ return $url;
+ }
+@@ -355,6 +370,9 @@ class Processor
+ if ((string)$local->report->trash) {
+ $config->trash = $local->report->trash;
+ }
++ if ($local->report->dir_nesting_level) {
++ $config->dir_nesting_level = (int)$local->report->dir_nesting_level;
++ }
+ if ((string)$local->skin) {
+ $this->_setSkin((string)$local->skin, $config);
+ }
+@@ -379,6 +397,8 @@ class Processor
+ }
+
+ /**
++ * Render page
++ *
+ * @param string $template
+ * @return string
+ */
+@@ -449,7 +469,7 @@ class Processor
+ $this->reportData['url'] = $this->getHostUrl() . $reportData['url'];
+ }
+
+- if ($this->reportData['script_name']) {
++ if (isset($this->reportData['script_name'])) {
+ $this->_scriptName = $this->reportData['script_name'];
+ }
+ }
+@@ -460,16 +480,18 @@ class Processor
+ * @param array $reportData
+ * @return string
+ */
+- public function saveReport($reportData)
++ public function saveReport(array $reportData): string
+ {
+- $this->reportData = $reportData;
+- $this->reportId = abs(intval(microtime(true) * rand(100, 1000)));
+- $this->_reportFile = $this->_reportDir . '/' . $this->reportId;
+- $this->_setReportData($reportData);
+-
+- if (!file_exists($this->_reportDir)) {
+- @mkdir($this->_reportDir, 0777, true);
++ $this->reportId = $reportData['report_id'];
++ $this->_reportFile = $this->getReportPath(
++ $this->getReportDirNestingLevel($this->reportId),
++ $this->reportId
++ );
++ $reportDirName = dirname($this->_reportFile);
++ if (!file_exists($reportDirName)) {
++ @mkdir($reportDirName, 0777, true);
+ }
++ $this->_setReportData($reportData);
+
+ @file_put_contents($this->_reportFile, $this->serializer->serialize($reportData));
+
+@@ -484,20 +506,118 @@ class Processor
+ /**
+ * Get report
+ *
+- * @param int $reportId
++ * @param string $reportId
+ * @return void
+- * @SuppressWarnings(PHPMD.ExitExpression)
+ */
+ public function loadReport($reportId)
+ {
+- $this->reportId = $reportId;
+- $this->_reportFile = $this->_reportDir . '/' . $reportId;
++ try {
++ if (!$this->isReportIdValid($reportId)) {
++ throw new \RuntimeException("Report Id is invalid");
++ }
++ $reportFile = $this->findReportFile($reportId);
++ if (!is_readable($reportFile)) {
++ throw new \RuntimeException("Report file cannot be read");
++ }
++ $this->reportId = $reportId;
++ $this->_reportFile = $reportFile;
++ $this->_setReportData($this->serializer->unserialize(file_get_contents($this->_reportFile)));
++ } catch (\RuntimeException $e) {
++ $this->redirectToBaseUrl();
++ }
++ }
++
++ /**
++ * Searches for the report file and returns the path to it
++ *
++ * @param string $reportId
++ * @return string
++ * @throws \RuntimeException
++ */
++ private function findReportFile(string $reportId): string
++ {
++ $reportFile = $this->getReportPath(
++ $this->getReportDirNestingLevel($reportId),
++ $reportId
++ );
++ if (file_exists($reportFile)) {
++ return $reportFile;
++ }
++ $maxReportDirNestingLevel = $this->getMaxReportDirNestingLevel($reportId);
++ for ($i = 0; $i <= $maxReportDirNestingLevel; $i++) {
++ $reportFile = $this->getReportPath($i, $reportId);
++ if (file_exists($reportFile)) {
++ return $reportFile;
++ }
++ }
++ throw new \RuntimeException("Report file not found");
++ }
++
++ /**
++ * Redirect to a base url
++ * @return void
++ * @SuppressWarnings(PHPMD.ExitExpression)
++ */
++ private function redirectToBaseUrl()
++ {
++ header("Location: " . $this->getBaseUrl());
++ die();
++ }
++
++ /**
++ * Checks report id
++ *
++ * @param string $reportId
++ * @return bool
++ */
++ private function isReportIdValid(string $reportId): bool
++ {
++ return (bool)preg_match('/[a-fA-F0-9]{64}/', $reportId);
++ }
++
++ /**
++ * Returns nesting Level for the report files
++ *
++ * @var $reportId
++ * @return int
++ */
++ private function getReportDirNestingLevel(string $reportId): int
++ {
++ $envName = 'MAGE_ERROR_REPORT_DIR_NESTING_LEVEL';
++ $value = $_ENV[$envName] ?? getenv($envName);
++ if (false === $value && property_exists($this->_config, 'dir_nesting_level')) {
++ $value = $this->_config->dir_nesting_level;
++ }
++ $value = (int)$value;
++ $maxValue = $this->getMaxReportDirNestingLevel($reportId);
++ return 0 < $value && $maxValue >= $value ? $value : 0;
++ }
++
++ /**
++ * Returns maximum nesting level directories of report files
++ *
++ * @param string $reportId
++ * @return integer
++ */
++ private function getMaxReportDirNestingLevel(string $reportId): int
++ {
++ return (int)floor(strlen($reportId) / self::NUMBER_SYMBOLS_IN_SUBDIR_NAME);
++ }
+
+- if (!file_exists($this->_reportFile) || !is_readable($this->_reportFile)) {
+- header("Location: " . $this->getBaseUrl());
+- die();
++ /**
++ * Get path to reports
++ *
++ * @param integer $reportDirNestingLevel
++ * @param string $reportId
++ * @return string
++ */
++ private function getReportPath(int $reportDirNestingLevel, string $reportId): string
++ {
++ $reportDirPath = $this->_reportDir;
++ for ($i = 0, $j = 0; $j < $reportDirNestingLevel; $i += 2, $j++) {
++ $reportDirPath .= $reportId[$i] . $reportId[$i + 1] . '/';
+ }
+- $this->_setReportData($this->serializer->unserialize(file_get_contents($this->_reportFile)));
++ return $reportDirPath . $reportId;
+ }
+
+ /**
+@@ -511,11 +631,16 @@ class Processor
+ {
+ $this->pageTitle = 'Error Submission Form';
+
+- $this->postData['firstName'] = (isset($_POST['firstname'])) ? trim(htmlspecialchars($_POST['firstname'])) : '';
+- $this->postData['lastName'] = (isset($_POST['lastname'])) ? trim(htmlspecialchars($_POST['lastname'])) : '';
+- $this->postData['email'] = (isset($_POST['email'])) ? trim(htmlspecialchars($_POST['email'])) : '';
+- $this->postData['telephone'] = (isset($_POST['telephone'])) ? trim(htmlspecialchars($_POST['telephone'])) : '';
+- $this->postData['comment'] = (isset($_POST['comment'])) ? trim(htmlspecialchars($_POST['comment'])) : '';
++ $this->postData['firstName'] = (isset($_POST['firstname']))
++ ? trim($this->escaper->escapeHtml($_POST['firstname'])) : '';
++ $this->postData['lastName'] = (isset($_POST['lastname']))
++ ? trim($this->escaper->escapeHtml($_POST['lastname'])) : '';
++ $this->postData['email'] = (isset($_POST['email']))
++ ? trim($this->escaper->escapeHtml($_POST['email'])) : '';
++ $this->postData['telephone'] = (isset($_POST['telephone']))
++ ? trim($this->escaper->escapeHtml($_POST['telephone'])) : '';
++ $this->postData['comment'] = (isset($_POST['comment']))
++ ? trim($this->escaper->escapeHtml($_POST['comment'])) : '';
+
+ if (isset($_POST['submit'])) {
+ if ($this->_validate()) {
diff --git a/patches/MAGECLOUD-3392__reduce_q-ty_of_error_report_files__2.2.5.patch b/patches/MAGECLOUD-3392__reduce_q-ty_of_error_report_files__2.2.5.patch
new file mode 100644
index 0000000..5ae791e
--- /dev/null
+++ b/patches/MAGECLOUD-3392__reduce_q-ty_of_error_report_files__2.2.5.patch
@@ -0,0 +1,996 @@
+diff -Naur a/app/etc/di.xml b/app/etc/di.xml
+--- a/app/etc/di.xml
++++ b/app/etc/di.xml
+@@ -44,6 +44,7 @@
+
+
+
++
+
+
+ system/currency/installed
+diff -Naur a/vendor/magento/framework/App/ExceptionHandler.php b/vendor/magento/framework/App/ExceptionHandler.php
+--- /dev/null
++++ b/vendor/magento/framework/App/ExceptionHandler.php
+@@ -0,0 +1,280 @@
++encryptor = $encryptor;
++ $this->filesystem = $filesystem;
++ $this->logger = $logger;
++ }
++
++ /**
++ * Handles exception of HTTP web application
++ *
++ * @param Bootstrap $bootstrap
++ * @param \Exception $exception
++ * @param ResponseHttp $response
++ * @param RequestHttp $request
++ * @return bool
++ */
++ public function handle(
++ Bootstrap $bootstrap,
++ \Exception $exception,
++ ResponseHttp $response,
++ RequestHttp $request
++ ): bool {
++ $result = $this->handleDeveloperMode($bootstrap, $exception, $response)
++ || $this->handleBootstrapErrors($bootstrap, $exception, $response)
++ || $this->handleSessionException($exception, $response, $request)
++ || $this->handleInitException($exception)
++ || $this->handleGenericReport($bootstrap, $exception);
++ return $result;
++ }
++
++ /**
++ * Error handler for developer mode
++ *
++ * @param Bootstrap $bootstrap
++ * @param \Exception $exception
++ * @param ResponseHttp $response
++ * @return bool
++ */
++ private function handleDeveloperMode(
++ Bootstrap $bootstrap,
++ \Exception $exception,
++ ResponseHttp $response
++ ): bool {
++ if ($bootstrap->isDeveloperMode()) {
++ if (Bootstrap::ERR_IS_INSTALLED == $bootstrap->getErrorCode()) {
++ try {
++ $this->redirectToSetup($bootstrap, $exception, $response);
++ return true;
++ } catch (\Exception $e) {
++ $exception = $e;
++ }
++ }
++ $response->setHttpResponseCode(500);
++ $response->setHeader('Content-Type', 'text/plain');
++ $response->setBody($this->buildContentFromException($exception));
++ $response->sendResponse();
++ return true;
++ }
++ return false;
++ }
++
++ /**
++ * Build content based on an exception
++ *
++ * @param \Exception $exception
++ * @return string
++ */
++ private function buildContentFromException(\Exception $exception) :string
++ {
++ /** @var \Exception[] $exceptions */
++ $exceptions = [];
++
++ do {
++ $exceptions[] = $exception;
++ } while ($exception = $exception->getPrevious());
++
++ $buffer = sprintf("%d exception(s):\n", count($exceptions));
++
++ foreach ($exceptions as $index => $exception) {
++ $buffer .= sprintf(
++ "Exception #%d (%s): %s\n",
++ $index,
++ get_class($exception),
++ $exception->getMessage()
++ );
++ }
++
++ foreach ($exceptions as $index => $exception) {
++ $buffer .= sprintf(
++ "\nException #%d (%s): %s\n%s\n",
++ $index,
++ get_class($exception),
++ $exception->getMessage(),
++ Debug::trace(
++ $exception->getTrace(),
++ true,
++ true,
++ (bool)getenv('MAGE_DEBUG_SHOW_ARGS')
++ )
++ );
++ }
++
++ return $buffer;
++ }
++
++ /**
++ * Handler for bootstrap errors
++ *
++ * @param Bootstrap $bootstrap
++ * @param \Exception &$exception
++ * @param ResponseHttp $response
++ * @return bool
++ */
++ private function handleBootstrapErrors(
++ Bootstrap $bootstrap,
++ \Exception &$exception,
++ ResponseHttp $response
++ ): bool {
++ $bootstrapCode = $bootstrap->getErrorCode();
++ if (Bootstrap::ERR_MAINTENANCE == $bootstrapCode) {
++ require $this->filesystem
++ ->getDirectoryRead(DirectoryList::PUB)
++ ->getAbsolutePath('errors/503.php');
++ return true;
++ }
++ if (Bootstrap::ERR_IS_INSTALLED == $bootstrapCode) {
++ try {
++ $this->redirectToSetup($bootstrap, $exception, $response);
++ return true;
++ } catch (\Exception $e) {
++ $exception = $e;
++ }
++ }
++ return false;
++ }
++
++ /**
++ * Handler for session errors
++ *
++ * @param \Exception $exception
++ * @param ResponseHttp $response
++ * @param RequestHttp $request
++ * @return bool
++ */
++ private function handleSessionException(
++ \Exception $exception,
++ ResponseHttp $response,
++ RequestHttp $request
++ ): bool {
++ if ($exception instanceof SessionException) {
++ $response->setRedirect($request->getDistroBaseUrl());
++ $response->sendHeaders();
++ return true;
++ }
++ return false;
++ }
++
++ /**
++ * Handler for application initialization errors
++ *
++ * @param \Exception $exception
++ * @return bool
++ */
++ private function handleInitException(\Exception $exception) : bool
++ {
++ if ($exception instanceof InitException) {
++ $this->logger->critical($exception);
++ require $this->filesystem
++ ->getDirectoryRead(DirectoryList::PUB)
++ ->getAbsolutePath('errors/404.php');
++ return true;
++ }
++ return false;
++ }
++
++ /**
++ * Handle for any other errors
++ *
++ * @param Bootstrap $bootstrap
++ * @param \Exception $exception
++ * @return bool
++ */
++ private function handleGenericReport(Bootstrap $bootstrap, \Exception $exception): bool
++ {
++ $reportData = [
++ $exception->getMessage(),
++ Debug::trace(
++ $exception->getTrace(),
++ true,
++ false,
++ (bool)getenv('MAGE_DEBUG_SHOW_ARGS')
++ )
++ ];
++ $params = $bootstrap->getParams();
++ if (isset($params['REQUEST_URI'])) {
++ $reportData['url'] = $params['REQUEST_URI'];
++ }
++ if (isset($params['SCRIPT_NAME'])) {
++ $reportData['script_name'] = $params['SCRIPT_NAME'];
++ }
++ $reportData['report_id'] = $this->encryptor->getHash(implode('', $reportData));
++ $this->logger->critical($exception, ['report_id' => $reportData['report_id']]);
++ require $this->filesystem
++ ->getDirectoryRead(DirectoryList::PUB)
++ ->getAbsolutePath('errors/report.php');
++ return true;
++ }
++ /**
++ * If not installed, try to redirect to installation wizard
++ *
++ * @param Bootstrap $bootstrap
++ * @param \Exception $exception
++ * @param ResponseHttp $response
++ * @return void
++ * @throws \Exception
++ */
++ private function redirectToSetup(Bootstrap $bootstrap, \Exception $exception, ResponseHttp $response)
++ {
++ $setupInfo = new SetupInfo($bootstrap->getParams());
++ $projectRoot = $this->filesystem->getDirectoryRead(DirectoryList::ROOT)->getAbsolutePath();
++ if ($setupInfo->isAvailable()) {
++ $response->setRedirect($setupInfo->getUrl());
++ $response->sendHeaders();
++ } else {
++ $newMessage = $exception->getMessage() . "\nNOTE: You cannot install Magento using the Setup Wizard "
++ . "because the Magento setup directory cannot be accessed. \n"
++ . 'You can install Magento using either the command line or you must restore access '
++ . 'to the following directory: ' . $setupInfo->getDir($projectRoot) . "\n";
++
++ throw new \Exception($newMessage, 0, $exception);
++ }
++ }
++}
+diff -Naur a/vendor/magento/framework/App/ExceptionHandlerInterface.php b/vendor/magento/framework/App/ExceptionHandlerInterface.php
+--- /dev/null
++++ b/vendor/magento/framework/App/ExceptionHandlerInterface.php
+@@ -0,0 +1,31 @@
++_objectManager = $objectManager;
+ $this->_eventManager = $eventManager;
+@@ -100,30 +97,15 @@ class Http implements \Magento\Framework\AppInterface
+ $this->_response = $response;
+ $this->_configLoader = $configLoader;
+ $this->_state = $state;
+- $this->_filesystem = $filesystem;
+ $this->registry = $registry;
+- }
+-
+- /**
+- * Add new dependency
+- *
+- * @return \Psr\Log\LoggerInterface
+- *
+- * @deprecated 100.1.0
+- */
+- private function getLogger()
+- {
+- if (!$this->logger instanceof \Psr\Log\LoggerInterface) {
+- $this->logger = \Magento\Framework\App\ObjectManager::getInstance()->get(\Psr\Log\LoggerInterface::class);
+- }
+- return $this->logger;
++ $this->exceptionHandler = $exceptionHandler ?: $this->_objectManager->get(ExceptionHandlerInterface::class);
+ }
+
+ /**
+ * Run application
+ *
+- * @throws \InvalidArgumentException
+ * @return ResponseInterface
++ * @throws \InvalidArgumentException|LocalizedException
+ */
+ public function launch()
+ {
+@@ -149,178 +131,10 @@ class Http implements \Magento\Framework\AppInterface
+ }
+
+ /**
+- * {@inheritdoc}
++ * @inheritdoc
+ */
+- public function catchException(Bootstrap $bootstrap, \Exception $exception)
++ public function catchException(Bootstrap $bootstrap, \Exception $exception): bool
+ {
+- $result = $this->handleDeveloperMode($bootstrap, $exception)
+- || $this->handleBootstrapErrors($bootstrap, $exception)
+- || $this->handleSessionException($exception)
+- || $this->handleInitException($exception)
+- || $this->handleGenericReport($bootstrap, $exception);
+- return $result;
+- }
+-
+- /**
+- * Error handler for developer mode
+- *
+- * @param Bootstrap $bootstrap
+- * @param \Exception $exception
+- * @return bool
+- */
+- private function handleDeveloperMode(Bootstrap $bootstrap, \Exception $exception)
+- {
+- if ($bootstrap->isDeveloperMode()) {
+- if (Bootstrap::ERR_IS_INSTALLED == $bootstrap->getErrorCode()) {
+- try {
+- $this->redirectToSetup($bootstrap, $exception);
+- return true;
+- } catch (\Exception $e) {
+- $exception = $e;
+- }
+- }
+- $this->_response->setHttpResponseCode(500);
+- $this->_response->setHeader('Content-Type', 'text/plain');
+- $this->_response->setBody($this->buildContentFromException($exception));
+- $this->_response->sendResponse();
+- return true;
+- }
+- return false;
+- }
+-
+- /**
+- * Build content based on an exception
+- *
+- * @param \Exception $exception
+- * @return string
+- */
+- private function buildContentFromException(\Exception $exception)
+- {
+- /** @var \Exception[] $exceptions */
+- $exceptions = [];
+- do {
+- $exceptions[] = $exception;
+- } while ($exception = $exception->getPrevious());
+-
+- $buffer = sprintf("%d exception(s):\n", count($exceptions));
+-
+- foreach ($exceptions as $index => $exception) {
+- $buffer .= sprintf("Exception #%d (%s): %s\n", $index, get_class($exception), $exception->getMessage());
+- }
+-
+- foreach ($exceptions as $index => $exception) {
+- $buffer .= sprintf(
+- "\nException #%d (%s): %s\n%s\n",
+- $index,
+- get_class($exception),
+- $exception->getMessage(),
+- $exception->getTraceAsString()
+- );
+- }
+-
+- return $buffer;
+- }
+-
+- /**
+- * If not installed, try to redirect to installation wizard
+- *
+- * @param Bootstrap $bootstrap
+- * @param \Exception $exception
+- * @return void
+- * @throws \Exception
+- */
+- private function redirectToSetup(Bootstrap $bootstrap, \Exception $exception)
+- {
+- $setupInfo = new SetupInfo($bootstrap->getParams());
+- $projectRoot = $this->_filesystem->getDirectoryRead(DirectoryList::ROOT)->getAbsolutePath();
+- if ($setupInfo->isAvailable()) {
+- $this->_response->setRedirect($setupInfo->getUrl());
+- $this->_response->sendHeaders();
+- } else {
+- $newMessage = $exception->getMessage() . "\nNOTE: You cannot install Magento using the Setup Wizard "
+- . "because the Magento setup directory cannot be accessed. \n"
+- . 'You can install Magento using either the command line or you must restore access '
+- . 'to the following directory: ' . $setupInfo->getDir($projectRoot) . "\n";
+-
+- throw new \Exception($newMessage, 0, $exception);
+- }
+- }
+-
+- /**
+- * Handler for bootstrap errors
+- *
+- * @param Bootstrap $bootstrap
+- * @param \Exception &$exception
+- * @return bool
+- */
+- private function handleBootstrapErrors(Bootstrap $bootstrap, \Exception &$exception)
+- {
+- $bootstrapCode = $bootstrap->getErrorCode();
+- if (Bootstrap::ERR_MAINTENANCE == $bootstrapCode) {
+- require $this->_filesystem->getDirectoryRead(DirectoryList::PUB)->getAbsolutePath('errors/503.php');
+- return true;
+- }
+- if (Bootstrap::ERR_IS_INSTALLED == $bootstrapCode) {
+- try {
+- $this->redirectToSetup($bootstrap, $exception);
+- return true;
+- } catch (\Exception $e) {
+- $exception = $e;
+- }
+- }
+- return false;
+- }
+-
+- /**
+- * Handler for session errors
+- *
+- * @param \Exception $exception
+- * @return bool
+- */
+- private function handleSessionException(\Exception $exception)
+- {
+- if ($exception instanceof \Magento\Framework\Exception\SessionException) {
+- $this->_response->setRedirect($this->_request->getDistroBaseUrl());
+- $this->_response->sendHeaders();
+- return true;
+- }
+- return false;
+- }
+-
+- /**
+- * Handler for application initialization errors
+- *
+- * @param \Exception $exception
+- * @return bool
+- */
+- private function handleInitException(\Exception $exception)
+- {
+- if ($exception instanceof \Magento\Framework\Exception\State\InitException) {
+- $this->getLogger()->critical($exception);
+- require $this->_filesystem->getDirectoryRead(DirectoryList::PUB)->getAbsolutePath('errors/404.php');
+- return true;
+- }
+- return false;
+- }
+-
+- /**
+- * Handle for any other errors
+- *
+- * @param Bootstrap $bootstrap
+- * @param \Exception $exception
+- * @return bool
+- */
+- private function handleGenericReport(Bootstrap $bootstrap, \Exception $exception)
+- {
+- $reportData = [$exception->getMessage(), $exception->getTraceAsString()];
+- $params = $bootstrap->getParams();
+- if (isset($params['REQUEST_URI'])) {
+- $reportData['url'] = $params['REQUEST_URI'];
+- }
+- if (isset($params['SCRIPT_NAME'])) {
+- $reportData['script_name'] = $params['SCRIPT_NAME'];
+- }
+- require $this->_filesystem->getDirectoryRead(DirectoryList::PUB)->getAbsolutePath('errors/report.php');
+- return true;
++ return $this->exceptionHandler->handle($bootstrap, $exception, $this->_response, $this->_request);
+ }
+ }
+diff -Naur a/pub/errors/local.xml.sample b/pub/errors/local.xml.sample
+--- a/pub/errors/local.xml.sample
++++ b/pub/errors/local.xml.sample
+@@ -27,5 +27,22 @@
+ value "delete" is for cleaning
+ -->
+ leave
++
++ 0
+
+
+diff -Naur a/pub/errors/processor.php b/pub/errors/processor.php
+--- a/pub/errors/processor.php
++++ b/pub/errors/processor.php
+@@ -6,11 +6,15 @@
+ namespace Magento\Framework\Error;
+
+ use Magento\Framework\Serialize\Serializer\Json;
++use Magento\Framework\Escaper;
++use Magento\Framework\App\ObjectManager;
++use Magento\Framework\App\Response\Http;
+
+ /**
+ * Error processor
+ *
+ * @SuppressWarnings(PHPMD.TooManyFields)
++ * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
+ */
+ class Processor
+ {
+@@ -18,6 +22,7 @@ class Processor
+ const MAGE_ERRORS_DESIGN_XML = 'design.xml';
+ const DEFAULT_SKIN = 'default';
+ const ERROR_DIR = 'pub/errors';
++ const NUMBER_SYMBOLS_IN_SUBDIR_NAME = 2;
+
+ /**
+ * Page title
+@@ -64,7 +69,7 @@ class Processor
+ /**
+ * Report ID
+ *
+- * @var int
++ * @var string
+ */
+ public $reportId;
+
+@@ -125,7 +130,7 @@ class Processor
+ /**
+ * Http response
+ *
+- * @var \Magento\Framework\App\Response\Http
++ * @var Http
+ */
+ protected $_response;
+
+@@ -137,15 +142,25 @@ class Processor
+ private $serializer;
+
+ /**
+- * @param \Magento\Framework\App\Response\Http $response
++ * @var Escaper
++ */
++ private $escaper;
++
++ /**
++ * @param Http $response
+ * @param Json $serializer
++ * @param Escaper $escaper
+ */
+- public function __construct(\Magento\Framework\App\Response\Http $response, Json $serializer = null)
+- {
++ public function __construct(
++ Http $response,
++ Json $serializer = null,
++ Escaper $escaper = null
++ ) {
+ $this->_response = $response;
+ $this->_errorDir = __DIR__ . '/';
+ $this->_reportDir = dirname(dirname($this->_errorDir)) . '/var/report/';
+- $this->serializer = $serializer ?: \Magento\Framework\App\ObjectManager::getInstance()->get(Json::class);
++ $this->serializer = $serializer ?: ObjectManager::getInstance()->get(Json::class);
++ $this->escaper = $escaper ?: ObjectManager::getInstance()->get(Escaper::class);
+
+ if (!empty($_SERVER['SCRIPT_NAME'])) {
+ if (in_array(basename($_SERVER['SCRIPT_NAME'], '.php'), ['404', '503', 'report'])) {
+@@ -155,11 +170,6 @@ class Processor
+ }
+ }
+
+- $reportId = (isset($_GET['id'])) ? (int)$_GET['id'] : null;
+- if ($reportId) {
+- $this->loadReport($reportId);
+- }
+-
+ $this->_indexDir = $this->_getIndexDir();
+ $this->_root = is_dir($this->_indexDir . 'app');
+
+@@ -167,6 +177,10 @@ class Processor
+ if (isset($_GET['skin'])) {
+ $this->_setSkin($_GET['skin']);
+ }
++
++ if (isset($_GET['id'])) {
++ $this->loadReport($_GET['id']);
++ }
+ }
+
+ /**
+@@ -268,10 +282,11 @@ class Processor
+ $isSecure = (!empty($_SERVER['HTTPS'])) && ($_SERVER['HTTPS'] != 'off');
+ $url = ($isSecure ? 'https://' : 'http://') . $host;
+
+- if (!empty($_SERVER['SERVER_PORT']) && !in_array($_SERVER['SERVER_PORT'], [80, 443])
++ $port = explode(':', $host);
++ if (isset($port[1]) && !in_array($port[1], [80, 443])
+ && !preg_match('/.*?\:[0-9]+$/', $url)
+ ) {
+- $url .= ':' . $_SERVER['SERVER_PORT'];
++ $url .= ':' . $port[1];
+ }
+ return $url;
+ }
+@@ -355,6 +370,9 @@ class Processor
+ if ((string)$local->report->trash) {
+ $config->trash = $local->report->trash;
+ }
++ if ($local->report->dir_nesting_level) {
++ $config->dir_nesting_level = (int)$local->report->dir_nesting_level;
++ }
+ if ((string)$local->skin) {
+ $this->_setSkin((string)$local->skin, $config);
+ }
+@@ -379,6 +397,8 @@ class Processor
+ }
+
+ /**
++ * Render page
++ *
+ * @param string $template
+ * @return string
+ */
+@@ -449,7 +469,7 @@ class Processor
+ $this->reportData['url'] = $this->getHostUrl() . $reportData['url'];
+ }
+
+- if ($this->reportData['script_name']) {
++ if (isset($this->reportData['script_name'])) {
+ $this->_scriptName = $this->reportData['script_name'];
+ }
+ }
+@@ -460,16 +480,18 @@ class Processor
+ * @param array $reportData
+ * @return string
+ */
+- public function saveReport($reportData)
++ public function saveReport(array $reportData): string
+ {
+- $this->reportData = $reportData;
+- $this->reportId = abs(intval(microtime(true) * rand(100, 1000)));
+- $this->_reportFile = $this->_reportDir . '/' . $this->reportId;
+- $this->_setReportData($reportData);
+-
+- if (!file_exists($this->_reportDir)) {
+- @mkdir($this->_reportDir, 0777, true);
++ $this->reportId = $reportData['report_id'];
++ $this->_reportFile = $this->getReportPath(
++ $this->getReportDirNestingLevel($this->reportId),
++ $this->reportId
++ );
++ $reportDirName = dirname($this->_reportFile);
++ if (!file_exists($reportDirName)) {
++ @mkdir($reportDirName, 0777, true);
+ }
++ $this->_setReportData($reportData);
+
+ @file_put_contents($this->_reportFile, $this->serializer->serialize($reportData));
+
+@@ -484,20 +506,118 @@ class Processor
+ /**
+ * Get report
+ *
+- * @param int $reportId
++ * @param string $reportId
+ * @return void
+- * @SuppressWarnings(PHPMD.ExitExpression)
+ */
+ public function loadReport($reportId)
+ {
+- $this->reportId = $reportId;
+- $this->_reportFile = $this->_reportDir . '/' . $reportId;
++ try {
++ if (!$this->isReportIdValid($reportId)) {
++ throw new \RuntimeException("Report Id is invalid");
++ }
++ $reportFile = $this->findReportFile($reportId);
++ if (!is_readable($reportFile)) {
++ throw new \RuntimeException("Report file cannot be read");
++ }
++ $this->reportId = $reportId;
++ $this->_reportFile = $reportFile;
++ $this->_setReportData($this->serializer->unserialize(file_get_contents($this->_reportFile)));
++ } catch (\RuntimeException $e) {
++ $this->redirectToBaseUrl();
++ }
++ }
++
++ /**
++ * Searches for the report file and returns the path to it
++ *
++ * @param string $reportId
++ * @return string
++ * @throws \RuntimeException
++ */
++ private function findReportFile(string $reportId): string
++ {
++ $reportFile = $this->getReportPath(
++ $this->getReportDirNestingLevel($reportId),
++ $reportId
++ );
++ if (file_exists($reportFile)) {
++ return $reportFile;
++ }
++ $maxReportDirNestingLevel = $this->getMaxReportDirNestingLevel($reportId);
++ for ($i = 0; $i <= $maxReportDirNestingLevel; $i++) {
++ $reportFile = $this->getReportPath($i, $reportId);
++ if (file_exists($reportFile)) {
++ return $reportFile;
++ }
++ }
++ throw new \RuntimeException("Report file not found");
++ }
++
++ /**
++ * Redirect to a base url
++ * @return void
++ * @SuppressWarnings(PHPMD.ExitExpression)
++ */
++ private function redirectToBaseUrl()
++ {
++ header("Location: " . $this->getBaseUrl());
++ die();
++ }
++
++ /**
++ * Checks report id
++ *
++ * @param string $reportId
++ * @return bool
++ */
++ private function isReportIdValid(string $reportId): bool
++ {
++ return (bool)preg_match('/[a-fA-F0-9]{64}/', $reportId);
++ }
++
++ /**
++ * Returns nesting Level for the report files
++ *
++ * @var $reportId
++ * @return int
++ */
++ private function getReportDirNestingLevel(string $reportId): int
++ {
++ $envName = 'MAGE_ERROR_REPORT_DIR_NESTING_LEVEL';
++ $value = $_ENV[$envName] ?? getenv($envName);
++ if (false === $value && property_exists($this->_config, 'dir_nesting_level')) {
++ $value = $this->_config->dir_nesting_level;
++ }
++ $value = (int)$value;
++ $maxValue = $this->getMaxReportDirNestingLevel($reportId);
++ return 0 < $value && $maxValue >= $value ? $value : 0;
++ }
++
++ /**
++ * Returns maximum nesting level directories of report files
++ *
++ * @param string $reportId
++ * @return integer
++ */
++ private function getMaxReportDirNestingLevel(string $reportId): int
++ {
++ return (int)floor(strlen($reportId) / self::NUMBER_SYMBOLS_IN_SUBDIR_NAME);
++ }
+
+- if (!file_exists($this->_reportFile) || !is_readable($this->_reportFile)) {
+- header("Location: " . $this->getBaseUrl());
+- die();
++ /**
++ * Get path to reports
++ *
++ * @param integer $reportDirNestingLevel
++ * @param string $reportId
++ * @return string
++ */
++ private function getReportPath(int $reportDirNestingLevel, string $reportId): string
++ {
++ $reportDirPath = $this->_reportDir;
++ for ($i = 0, $j = 0; $j < $reportDirNestingLevel; $i += 2, $j++) {
++ $reportDirPath .= $reportId[$i] . $reportId[$i + 1] . '/';
+ }
+- $this->_setReportData($this->serializer->unserialize(file_get_contents($this->_reportFile)));
++ return $reportDirPath . $reportId;
+ }
+
+ /**
+@@ -511,11 +631,16 @@ class Processor
+ {
+ $this->pageTitle = 'Error Submission Form';
+
+- $this->postData['firstName'] = (isset($_POST['firstname'])) ? trim(htmlspecialchars($_POST['firstname'])) : '';
+- $this->postData['lastName'] = (isset($_POST['lastname'])) ? trim(htmlspecialchars($_POST['lastname'])) : '';
+- $this->postData['email'] = (isset($_POST['email'])) ? trim(htmlspecialchars($_POST['email'])) : '';
+- $this->postData['telephone'] = (isset($_POST['telephone'])) ? trim(htmlspecialchars($_POST['telephone'])) : '';
+- $this->postData['comment'] = (isset($_POST['comment'])) ? trim(htmlspecialchars($_POST['comment'])) : '';
++ $this->postData['firstName'] = (isset($_POST['firstname']))
++ ? trim($this->escaper->escapeHtml($_POST['firstname'])) : '';
++ $this->postData['lastName'] = (isset($_POST['lastname']))
++ ? trim($this->escaper->escapeHtml($_POST['lastname'])) : '';
++ $this->postData['email'] = (isset($_POST['email']))
++ ? trim($this->escaper->escapeHtml($_POST['email'])) : '';
++ $this->postData['telephone'] = (isset($_POST['telephone']))
++ ? trim($this->escaper->escapeHtml($_POST['telephone'])) : '';
++ $this->postData['comment'] = (isset($_POST['comment']))
++ ? trim($this->escaper->escapeHtml($_POST['comment'])) : '';
+
+ if (isset($_POST['submit'])) {
+ if ($this->_validate()) {
diff --git a/patches/MAGECLOUD-3392__reduce_q-ty_of_error_report_files__2.2.6.patch b/patches/MAGECLOUD-3392__reduce_q-ty_of_error_report_files__2.2.6.patch
new file mode 100644
index 0000000..91e8ebc
--- /dev/null
+++ b/patches/MAGECLOUD-3392__reduce_q-ty_of_error_report_files__2.2.6.patch
@@ -0,0 +1,996 @@
+diff -Naur a/app/etc/di.xml b/app/etc/di.xml
+--- a/app/etc/di.xml
++++ b/app/etc/di.xml
+@@ -44,6 +44,7 @@
+
+
+
++
+
+
+ system/currency/installed
+diff -Naur a/vendor/magento/framework/App/ExceptionHandler.php b/vendor/magento/framework/App/ExceptionHandler.php
+--- /dev/null
++++ b/vendor/magento/framework/App/ExceptionHandler.php
+@@ -0,0 +1,280 @@
++encryptor = $encryptor;
++ $this->filesystem = $filesystem;
++ $this->logger = $logger;
++ }
++
++ /**
++ * Handles exception of HTTP web application
++ *
++ * @param Bootstrap $bootstrap
++ * @param \Exception $exception
++ * @param ResponseHttp $response
++ * @param RequestHttp $request
++ * @return bool
++ */
++ public function handle(
++ Bootstrap $bootstrap,
++ \Exception $exception,
++ ResponseHttp $response,
++ RequestHttp $request
++ ): bool {
++ $result = $this->handleDeveloperMode($bootstrap, $exception, $response)
++ || $this->handleBootstrapErrors($bootstrap, $exception, $response)
++ || $this->handleSessionException($exception, $response, $request)
++ || $this->handleInitException($exception)
++ || $this->handleGenericReport($bootstrap, $exception);
++ return $result;
++ }
++
++ /**
++ * Error handler for developer mode
++ *
++ * @param Bootstrap $bootstrap
++ * @param \Exception $exception
++ * @param ResponseHttp $response
++ * @return bool
++ */
++ private function handleDeveloperMode(
++ Bootstrap $bootstrap,
++ \Exception $exception,
++ ResponseHttp $response
++ ): bool {
++ if ($bootstrap->isDeveloperMode()) {
++ if (Bootstrap::ERR_IS_INSTALLED == $bootstrap->getErrorCode()) {
++ try {
++ $this->redirectToSetup($bootstrap, $exception, $response);
++ return true;
++ } catch (\Exception $e) {
++ $exception = $e;
++ }
++ }
++ $response->setHttpResponseCode(500);
++ $response->setHeader('Content-Type', 'text/plain');
++ $response->setBody($this->buildContentFromException($exception));
++ $response->sendResponse();
++ return true;
++ }
++ return false;
++ }
++
++ /**
++ * Build content based on an exception
++ *
++ * @param \Exception $exception
++ * @return string
++ */
++ private function buildContentFromException(\Exception $exception) :string
++ {
++ /** @var \Exception[] $exceptions */
++ $exceptions = [];
++
++ do {
++ $exceptions[] = $exception;
++ } while ($exception = $exception->getPrevious());
++
++ $buffer = sprintf("%d exception(s):\n", count($exceptions));
++
++ foreach ($exceptions as $index => $exception) {
++ $buffer .= sprintf(
++ "Exception #%d (%s): %s\n",
++ $index,
++ get_class($exception),
++ $exception->getMessage()
++ );
++ }
++
++ foreach ($exceptions as $index => $exception) {
++ $buffer .= sprintf(
++ "\nException #%d (%s): %s\n%s\n",
++ $index,
++ get_class($exception),
++ $exception->getMessage(),
++ Debug::trace(
++ $exception->getTrace(),
++ true,
++ true,
++ (bool)getenv('MAGE_DEBUG_SHOW_ARGS')
++ )
++ );
++ }
++
++ return $buffer;
++ }
++
++ /**
++ * Handler for bootstrap errors
++ *
++ * @param Bootstrap $bootstrap
++ * @param \Exception &$exception
++ * @param ResponseHttp $response
++ * @return bool
++ */
++ private function handleBootstrapErrors(
++ Bootstrap $bootstrap,
++ \Exception &$exception,
++ ResponseHttp $response
++ ): bool {
++ $bootstrapCode = $bootstrap->getErrorCode();
++ if (Bootstrap::ERR_MAINTENANCE == $bootstrapCode) {
++ require $this->filesystem
++ ->getDirectoryRead(DirectoryList::PUB)
++ ->getAbsolutePath('errors/503.php');
++ return true;
++ }
++ if (Bootstrap::ERR_IS_INSTALLED == $bootstrapCode) {
++ try {
++ $this->redirectToSetup($bootstrap, $exception, $response);
++ return true;
++ } catch (\Exception $e) {
++ $exception = $e;
++ }
++ }
++ return false;
++ }
++
++ /**
++ * Handler for session errors
++ *
++ * @param \Exception $exception
++ * @param ResponseHttp $response
++ * @param RequestHttp $request
++ * @return bool
++ */
++ private function handleSessionException(
++ \Exception $exception,
++ ResponseHttp $response,
++ RequestHttp $request
++ ): bool {
++ if ($exception instanceof SessionException) {
++ $response->setRedirect($request->getDistroBaseUrl());
++ $response->sendHeaders();
++ return true;
++ }
++ return false;
++ }
++
++ /**
++ * Handler for application initialization errors
++ *
++ * @param \Exception $exception
++ * @return bool
++ */
++ private function handleInitException(\Exception $exception) : bool
++ {
++ if ($exception instanceof InitException) {
++ $this->logger->critical($exception);
++ require $this->filesystem
++ ->getDirectoryRead(DirectoryList::PUB)
++ ->getAbsolutePath('errors/404.php');
++ return true;
++ }
++ return false;
++ }
++
++ /**
++ * Handle for any other errors
++ *
++ * @param Bootstrap $bootstrap
++ * @param \Exception $exception
++ * @return bool
++ */
++ private function handleGenericReport(Bootstrap $bootstrap, \Exception $exception): bool
++ {
++ $reportData = [
++ $exception->getMessage(),
++ Debug::trace(
++ $exception->getTrace(),
++ true,
++ false,
++ (bool)getenv('MAGE_DEBUG_SHOW_ARGS')
++ )
++ ];
++ $params = $bootstrap->getParams();
++ if (isset($params['REQUEST_URI'])) {
++ $reportData['url'] = $params['REQUEST_URI'];
++ }
++ if (isset($params['SCRIPT_NAME'])) {
++ $reportData['script_name'] = $params['SCRIPT_NAME'];
++ }
++ $reportData['report_id'] = $this->encryptor->getHash(implode('', $reportData));
++ $this->logger->critical($exception, ['report_id' => $reportData['report_id']]);
++ require $this->filesystem
++ ->getDirectoryRead(DirectoryList::PUB)
++ ->getAbsolutePath('errors/report.php');
++ return true;
++ }
++ /**
++ * If not installed, try to redirect to installation wizard
++ *
++ * @param Bootstrap $bootstrap
++ * @param \Exception $exception
++ * @param ResponseHttp $response
++ * @return void
++ * @throws \Exception
++ */
++ private function redirectToSetup(Bootstrap $bootstrap, \Exception $exception, ResponseHttp $response)
++ {
++ $setupInfo = new SetupInfo($bootstrap->getParams());
++ $projectRoot = $this->filesystem->getDirectoryRead(DirectoryList::ROOT)->getAbsolutePath();
++ if ($setupInfo->isAvailable()) {
++ $response->setRedirect($setupInfo->getUrl());
++ $response->sendHeaders();
++ } else {
++ $newMessage = $exception->getMessage() . "\nNOTE: You cannot install Magento using the Setup Wizard "
++ . "because the Magento setup directory cannot be accessed. \n"
++ . 'You can install Magento using either the command line or you must restore access '
++ . 'to the following directory: ' . $setupInfo->getDir($projectRoot) . "\n";
++
++ throw new \Exception($newMessage, 0, $exception);
++ }
++ }
++}
+diff -Naur a/vendor/magento/framework/App/ExceptionHandlerInterface.php b/vendor/magento/framework/App/ExceptionHandlerInterface.php
+--- /dev/null
++++ b/vendor/magento/framework/App/ExceptionHandlerInterface.php
+@@ -0,0 +1,31 @@
++_objectManager = $objectManager;
+ $this->_eventManager = $eventManager;
+@@ -100,30 +97,15 @@ class Http implements \Magento\Framework\AppInterface
+ $this->_response = $response;
+ $this->_configLoader = $configLoader;
+ $this->_state = $state;
+- $this->_filesystem = $filesystem;
+ $this->registry = $registry;
+- }
+-
+- /**
+- * Add new dependency
+- *
+- * @return \Psr\Log\LoggerInterface
+- *
+- * @deprecated 100.1.0
+- */
+- private function getLogger()
+- {
+- if (!$this->logger instanceof \Psr\Log\LoggerInterface) {
+- $this->logger = \Magento\Framework\App\ObjectManager::getInstance()->get(\Psr\Log\LoggerInterface::class);
+- }
+- return $this->logger;
++ $this->exceptionHandler = $exceptionHandler ?: $this->_objectManager->get(ExceptionHandlerInterface::class);
+ }
+
+ /**
+ * Run application
+ *
+- * @throws \InvalidArgumentException
+ * @return ResponseInterface
++ * @throws \InvalidArgumentException|LocalizedException
+ */
+ public function launch()
+ {
+@@ -149,178 +131,10 @@ class Http implements \Magento\Framework\AppInterface
+ }
+
+ /**
+- * {@inheritdoc}
++ * @inheritdoc
+ */
+- public function catchException(Bootstrap $bootstrap, \Exception $exception)
++ public function catchException(Bootstrap $bootstrap, \Exception $exception): bool
+ {
+- $result = $this->handleDeveloperMode($bootstrap, $exception)
+- || $this->handleBootstrapErrors($bootstrap, $exception)
+- || $this->handleSessionException($exception)
+- || $this->handleInitException($exception)
+- || $this->handleGenericReport($bootstrap, $exception);
+- return $result;
+- }
+-
+- /**
+- * Error handler for developer mode
+- *
+- * @param Bootstrap $bootstrap
+- * @param \Exception $exception
+- * @return bool
+- */
+- private function handleDeveloperMode(Bootstrap $bootstrap, \Exception $exception)
+- {
+- if ($bootstrap->isDeveloperMode()) {
+- if (Bootstrap::ERR_IS_INSTALLED == $bootstrap->getErrorCode()) {
+- try {
+- $this->redirectToSetup($bootstrap, $exception);
+- return true;
+- } catch (\Exception $e) {
+- $exception = $e;
+- }
+- }
+- $this->_response->setHttpResponseCode(500);
+- $this->_response->setHeader('Content-Type', 'text/plain');
+- $this->_response->setBody($this->buildContentFromException($exception));
+- $this->_response->sendResponse();
+- return true;
+- }
+- return false;
+- }
+-
+- /**
+- * Build content based on an exception
+- *
+- * @param \Exception $exception
+- * @return string
+- */
+- private function buildContentFromException(\Exception $exception)
+- {
+- /** @var \Exception[] $exceptions */
+- $exceptions = [];
+- do {
+- $exceptions[] = $exception;
+- } while ($exception = $exception->getPrevious());
+-
+- $buffer = sprintf("%d exception(s):\n", count($exceptions));
+-
+- foreach ($exceptions as $index => $exception) {
+- $buffer .= sprintf("Exception #%d (%s): %s\n", $index, get_class($exception), $exception->getMessage());
+- }
+-
+- foreach ($exceptions as $index => $exception) {
+- $buffer .= sprintf(
+- "\nException #%d (%s): %s\n%s\n",
+- $index,
+- get_class($exception),
+- $exception->getMessage(),
+- $exception->getTraceAsString()
+- );
+- }
+-
+- return $buffer;
+- }
+-
+- /**
+- * If not installed, try to redirect to installation wizard
+- *
+- * @param Bootstrap $bootstrap
+- * @param \Exception $exception
+- * @return void
+- * @throws \Exception
+- */
+- private function redirectToSetup(Bootstrap $bootstrap, \Exception $exception)
+- {
+- $setupInfo = new SetupInfo($bootstrap->getParams());
+- $projectRoot = $this->_filesystem->getDirectoryRead(DirectoryList::ROOT)->getAbsolutePath();
+- if ($setupInfo->isAvailable()) {
+- $this->_response->setRedirect($setupInfo->getUrl());
+- $this->_response->sendHeaders();
+- } else {
+- $newMessage = $exception->getMessage() . "\nNOTE: You cannot install Magento using the Setup Wizard "
+- . "because the Magento setup directory cannot be accessed. \n"
+- . 'You can install Magento using either the command line or you must restore access '
+- . 'to the following directory: ' . $setupInfo->getDir($projectRoot) . "\n";
+-
+- throw new \Exception($newMessage, 0, $exception);
+- }
+- }
+-
+- /**
+- * Handler for bootstrap errors
+- *
+- * @param Bootstrap $bootstrap
+- * @param \Exception &$exception
+- * @return bool
+- */
+- private function handleBootstrapErrors(Bootstrap $bootstrap, \Exception &$exception)
+- {
+- $bootstrapCode = $bootstrap->getErrorCode();
+- if (Bootstrap::ERR_MAINTENANCE == $bootstrapCode) {
+- require $this->_filesystem->getDirectoryRead(DirectoryList::PUB)->getAbsolutePath('errors/503.php');
+- return true;
+- }
+- if (Bootstrap::ERR_IS_INSTALLED == $bootstrapCode) {
+- try {
+- $this->redirectToSetup($bootstrap, $exception);
+- return true;
+- } catch (\Exception $e) {
+- $exception = $e;
+- }
+- }
+- return false;
+- }
+-
+- /**
+- * Handler for session errors
+- *
+- * @param \Exception $exception
+- * @return bool
+- */
+- private function handleSessionException(\Exception $exception)
+- {
+- if ($exception instanceof \Magento\Framework\Exception\SessionException) {
+- $this->_response->setRedirect($this->_request->getDistroBaseUrl());
+- $this->_response->sendHeaders();
+- return true;
+- }
+- return false;
+- }
+-
+- /**
+- * Handler for application initialization errors
+- *
+- * @param \Exception $exception
+- * @return bool
+- */
+- private function handleInitException(\Exception $exception)
+- {
+- if ($exception instanceof \Magento\Framework\Exception\State\InitException) {
+- $this->getLogger()->critical($exception);
+- require $this->_filesystem->getDirectoryRead(DirectoryList::PUB)->getAbsolutePath('errors/404.php');
+- return true;
+- }
+- return false;
+- }
+-
+- /**
+- * Handle for any other errors
+- *
+- * @param Bootstrap $bootstrap
+- * @param \Exception $exception
+- * @return bool
+- */
+- private function handleGenericReport(Bootstrap $bootstrap, \Exception $exception)
+- {
+- $reportData = [$exception->getMessage(), $exception->getTraceAsString()];
+- $params = $bootstrap->getParams();
+- if (isset($params['REQUEST_URI'])) {
+- $reportData['url'] = $params['REQUEST_URI'];
+- }
+- if (isset($params['SCRIPT_NAME'])) {
+- $reportData['script_name'] = $params['SCRIPT_NAME'];
+- }
+- require $this->_filesystem->getDirectoryRead(DirectoryList::PUB)->getAbsolutePath('errors/report.php');
+- return true;
++ return $this->exceptionHandler->handle($bootstrap, $exception, $this->_response, $this->_request);
+ }
+ }
+diff -Naur a/pub/errors/local.xml.sample b/pub/errors/local.xml.sample
+--- a/pub/errors/local.xml.sample
++++ b/pub/errors/local.xml.sample
+@@ -27,5 +27,22 @@
+ value "delete" is for cleaning
+ -->
+ leave
++
++ 0
+
+
+diff -Naur a/pub/errors/processor.php b/pub/errors/processor.php
+--- a/pub/errors/processor.php
++++ b/pub/errors/processor.php
+@@ -6,11 +6,15 @@
+ namespace Magento\Framework\Error;
+
+ use Magento\Framework\Serialize\Serializer\Json;
++use Magento\Framework\Escaper;
++use Magento\Framework\App\ObjectManager;
++use Magento\Framework\App\Response\Http;
+
+ /**
+ * Error processor
+ *
+ * @SuppressWarnings(PHPMD.TooManyFields)
++ * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
+ */
+ class Processor
+ {
+@@ -18,6 +22,7 @@ class Processor
+ const MAGE_ERRORS_DESIGN_XML = 'design.xml';
+ const DEFAULT_SKIN = 'default';
+ const ERROR_DIR = 'pub/errors';
++ const NUMBER_SYMBOLS_IN_SUBDIR_NAME = 2;
+
+ /**
+ * Page title
+@@ -64,7 +69,7 @@ class Processor
+ /**
+ * Report ID
+ *
+- * @var int
++ * @var string
+ */
+ public $reportId;
+
+@@ -125,7 +130,7 @@ class Processor
+ /**
+ * Http response
+ *
+- * @var \Magento\Framework\App\Response\Http
++ * @var Http
+ */
+ protected $_response;
+
+@@ -137,15 +142,25 @@ class Processor
+ private $serializer;
+
+ /**
+- * @param \Magento\Framework\App\Response\Http $response
++ * @var Escaper
++ */
++ private $escaper;
++
++ /**
++ * @param Http $response
+ * @param Json $serializer
++ * @param Escaper $escaper
+ */
+- public function __construct(\Magento\Framework\App\Response\Http $response, Json $serializer = null)
+- {
++ public function __construct(
++ Http $response,
++ Json $serializer = null,
++ Escaper $escaper = null
++ ) {
+ $this->_response = $response;
+ $this->_errorDir = __DIR__ . '/';
+ $this->_reportDir = dirname(dirname($this->_errorDir)) . '/var/report/';
+- $this->serializer = $serializer ?: \Magento\Framework\App\ObjectManager::getInstance()->get(Json::class);
++ $this->serializer = $serializer ?: ObjectManager::getInstance()->get(Json::class);
++ $this->escaper = $escaper ?: ObjectManager::getInstance()->get(Escaper::class);
+
+ if (!empty($_SERVER['SCRIPT_NAME'])) {
+ if (in_array(basename($_SERVER['SCRIPT_NAME'], '.php'), ['404', '503', 'report'])) {
+@@ -155,11 +170,6 @@ class Processor
+ }
+ }
+
+- $reportId = (isset($_GET['id'])) ? (int)$_GET['id'] : null;
+- if ($reportId) {
+- $this->loadReport($reportId);
+- }
+-
+ $this->_indexDir = $this->_getIndexDir();
+ $this->_root = is_dir($this->_indexDir . 'app');
+
+@@ -167,6 +177,10 @@ class Processor
+ if (isset($_GET['skin'])) {
+ $this->_setSkin($_GET['skin']);
+ }
++
++ if (isset($_GET['id'])) {
++ $this->loadReport($_GET['id']);
++ }
+ }
+
+ /**
+@@ -268,10 +282,11 @@ class Processor
+ $isSecure = (!empty($_SERVER['HTTPS'])) && ($_SERVER['HTTPS'] != 'off');
+ $url = ($isSecure ? 'https://' : 'http://') . $host;
+
+- if (!empty($_SERVER['SERVER_PORT']) && !in_array($_SERVER['SERVER_PORT'], [80, 443])
++ $port = explode(':', $host);
++ if (isset($port[1]) && !in_array($port[1], [80, 443])
+ && !preg_match('/.*?\:[0-9]+$/', $url)
+ ) {
+- $url .= ':' . $_SERVER['SERVER_PORT'];
++ $url .= ':' . $port[1];
+ }
+ return $url;
+ }
+@@ -355,6 +370,9 @@ class Processor
+ if ((string)$local->report->trash) {
+ $config->trash = $local->report->trash;
+ }
++ if ($local->report->dir_nesting_level) {
++ $config->dir_nesting_level = (int)$local->report->dir_nesting_level;
++ }
+ if ((string)$local->skin) {
+ $this->_setSkin((string)$local->skin, $config);
+ }
+@@ -379,6 +397,8 @@ class Processor
+ }
+
+ /**
++ * Render page
++ *
+ * @param string $template
+ * @return string
+ */
+@@ -449,7 +469,7 @@ class Processor
+ $this->reportData['url'] = $this->getHostUrl() . $reportData['url'];
+ }
+
+- if ($this->reportData['script_name']) {
++ if (isset($this->reportData['script_name'])) {
+ $this->_scriptName = $this->reportData['script_name'];
+ }
+ }
+@@ -460,16 +480,18 @@ class Processor
+ * @param array $reportData
+ * @return string
+ */
+- public function saveReport($reportData)
++ public function saveReport(array $reportData): string
+ {
+- $this->reportData = $reportData;
+- $this->reportId = abs(intval(microtime(true) * random_int(100, 1000)));
+- $this->_reportFile = $this->_reportDir . '/' . $this->reportId;
+- $this->_setReportData($reportData);
+-
+- if (!file_exists($this->_reportDir)) {
+- @mkdir($this->_reportDir, 0777, true);
++ $this->reportId = $reportData['report_id'];
++ $this->_reportFile = $this->getReportPath(
++ $this->getReportDirNestingLevel($this->reportId),
++ $this->reportId
++ );
++ $reportDirName = dirname($this->_reportFile);
++ if (!file_exists($reportDirName)) {
++ @mkdir($reportDirName, 0777, true);
+ }
++ $this->_setReportData($reportData);
+
+ @file_put_contents($this->_reportFile, $this->serializer->serialize($reportData));
+
+@@ -484,20 +506,118 @@ class Processor
+ /**
+ * Get report
+ *
+- * @param int $reportId
++ * @param string $reportId
+ * @return void
+- * @SuppressWarnings(PHPMD.ExitExpression)
+ */
+ public function loadReport($reportId)
+ {
+- $this->reportId = $reportId;
+- $this->_reportFile = $this->_reportDir . '/' . $reportId;
++ try {
++ if (!$this->isReportIdValid($reportId)) {
++ throw new \RuntimeException("Report Id is invalid");
++ }
++ $reportFile = $this->findReportFile($reportId);
++ if (!is_readable($reportFile)) {
++ throw new \RuntimeException("Report file cannot be read");
++ }
++ $this->reportId = $reportId;
++ $this->_reportFile = $reportFile;
++ $this->_setReportData($this->serializer->unserialize(file_get_contents($this->_reportFile)));
++ } catch (\RuntimeException $e) {
++ $this->redirectToBaseUrl();
++ }
++ }
++
++ /**
++ * Searches for the report file and returns the path to it
++ *
++ * @param string $reportId
++ * @return string
++ * @throws \RuntimeException
++ */
++ private function findReportFile(string $reportId): string
++ {
++ $reportFile = $this->getReportPath(
++ $this->getReportDirNestingLevel($reportId),
++ $reportId
++ );
++ if (file_exists($reportFile)) {
++ return $reportFile;
++ }
++ $maxReportDirNestingLevel = $this->getMaxReportDirNestingLevel($reportId);
++ for ($i = 0; $i <= $maxReportDirNestingLevel; $i++) {
++ $reportFile = $this->getReportPath($i, $reportId);
++ if (file_exists($reportFile)) {
++ return $reportFile;
++ }
++ }
++ throw new \RuntimeException("Report file not found");
++ }
++
++ /**
++ * Redirect to a base url
++ * @return void
++ * @SuppressWarnings(PHPMD.ExitExpression)
++ */
++ private function redirectToBaseUrl()
++ {
++ header("Location: " . $this->getBaseUrl());
++ die();
++ }
++
++ /**
++ * Checks report id
++ *
++ * @param string $reportId
++ * @return bool
++ */
++ private function isReportIdValid(string $reportId): bool
++ {
++ return (bool)preg_match('/[a-fA-F0-9]{64}/', $reportId);
++ }
++
++ /**
++ * Returns nesting Level for the report files
++ *
++ * @var $reportId
++ * @return int
++ */
++ private function getReportDirNestingLevel(string $reportId): int
++ {
++ $envName = 'MAGE_ERROR_REPORT_DIR_NESTING_LEVEL';
++ $value = $_ENV[$envName] ?? getenv($envName);
++ if (false === $value && property_exists($this->_config, 'dir_nesting_level')) {
++ $value = $this->_config->dir_nesting_level;
++ }
++ $value = (int)$value;
++ $maxValue = $this->getMaxReportDirNestingLevel($reportId);
++ return 0 < $value && $maxValue >= $value ? $value : 0;
++ }
++
++ /**
++ * Returns maximum nesting level directories of report files
++ *
++ * @param string $reportId
++ * @return integer
++ */
++ private function getMaxReportDirNestingLevel(string $reportId): int
++ {
++ return (int)floor(strlen($reportId) / self::NUMBER_SYMBOLS_IN_SUBDIR_NAME);
++ }
+
+- if (!file_exists($this->_reportFile) || !is_readable($this->_reportFile)) {
+- header("Location: " . $this->getBaseUrl());
+- die();
++ /**
++ * Get path to reports
++ *
++ * @param integer $reportDirNestingLevel
++ * @param string $reportId
++ * @return string
++ */
++ private function getReportPath(int $reportDirNestingLevel, string $reportId): string
++ {
++ $reportDirPath = $this->_reportDir;
++ for ($i = 0, $j = 0; $j < $reportDirNestingLevel; $i += 2, $j++) {
++ $reportDirPath .= $reportId[$i] . $reportId[$i + 1] . '/';
+ }
+- $this->_setReportData($this->serializer->unserialize(file_get_contents($this->_reportFile)));
++ return $reportDirPath . $reportId;
+ }
+
+ /**
+@@ -511,11 +631,16 @@ class Processor
+ {
+ $this->pageTitle = 'Error Submission Form';
+
+- $this->postData['firstName'] = (isset($_POST['firstname'])) ? trim(htmlspecialchars($_POST['firstname'])) : '';
+- $this->postData['lastName'] = (isset($_POST['lastname'])) ? trim(htmlspecialchars($_POST['lastname'])) : '';
+- $this->postData['email'] = (isset($_POST['email'])) ? trim(htmlspecialchars($_POST['email'])) : '';
+- $this->postData['telephone'] = (isset($_POST['telephone'])) ? trim(htmlspecialchars($_POST['telephone'])) : '';
+- $this->postData['comment'] = (isset($_POST['comment'])) ? trim(htmlspecialchars($_POST['comment'])) : '';
++ $this->postData['firstName'] = (isset($_POST['firstname']))
++ ? trim($this->escaper->escapeHtml($_POST['firstname'])) : '';
++ $this->postData['lastName'] = (isset($_POST['lastname']))
++ ? trim($this->escaper->escapeHtml($_POST['lastname'])) : '';
++ $this->postData['email'] = (isset($_POST['email']))
++ ? trim($this->escaper->escapeHtml($_POST['email'])) : '';
++ $this->postData['telephone'] = (isset($_POST['telephone']))
++ ? trim($this->escaper->escapeHtml($_POST['telephone'])) : '';
++ $this->postData['comment'] = (isset($_POST['comment']))
++ ? trim($this->escaper->escapeHtml($_POST['comment'])) : '';
+
+ if (isset($_POST['submit'])) {
+ if ($this->_validate()) {
diff --git a/patches/MAGECLOUD-3392__reduce_q-ty_of_error_report_files__2.2.8.patch b/patches/MAGECLOUD-3392__reduce_q-ty_of_error_report_files__2.2.8.patch
new file mode 100644
index 0000000..5559c0e
--- /dev/null
+++ b/patches/MAGECLOUD-3392__reduce_q-ty_of_error_report_files__2.2.8.patch
@@ -0,0 +1,985 @@
+diff -Naur a/app/etc/di.xml b/app/etc/di.xml
+--- a/app/etc/di.xml
++++ b/app/etc/di.xml
+@@ -44,6 +44,7 @@
+
+
+
++
+
+
+ system/currency/installed
+diff -Naur a/vendor/magento/framework/App/ExceptionHandler.php b/vendor/magento/framework/App/ExceptionHandler.php
+--- /dev/null
++++ b/vendor/magento/framework/App/ExceptionHandler.php
+@@ -0,0 +1,280 @@
++encryptor = $encryptor;
++ $this->filesystem = $filesystem;
++ $this->logger = $logger;
++ }
++
++ /**
++ * Handles exception of HTTP web application
++ *
++ * @param Bootstrap $bootstrap
++ * @param \Exception $exception
++ * @param ResponseHttp $response
++ * @param RequestHttp $request
++ * @return bool
++ */
++ public function handle(
++ Bootstrap $bootstrap,
++ \Exception $exception,
++ ResponseHttp $response,
++ RequestHttp $request
++ ): bool {
++ $result = $this->handleDeveloperMode($bootstrap, $exception, $response)
++ || $this->handleBootstrapErrors($bootstrap, $exception, $response)
++ || $this->handleSessionException($exception, $response, $request)
++ || $this->handleInitException($exception)
++ || $this->handleGenericReport($bootstrap, $exception);
++ return $result;
++ }
++
++ /**
++ * Error handler for developer mode
++ *
++ * @param Bootstrap $bootstrap
++ * @param \Exception $exception
++ * @param ResponseHttp $response
++ * @return bool
++ */
++ private function handleDeveloperMode(
++ Bootstrap $bootstrap,
++ \Exception $exception,
++ ResponseHttp $response
++ ): bool {
++ if ($bootstrap->isDeveloperMode()) {
++ if (Bootstrap::ERR_IS_INSTALLED == $bootstrap->getErrorCode()) {
++ try {
++ $this->redirectToSetup($bootstrap, $exception, $response);
++ return true;
++ } catch (\Exception $e) {
++ $exception = $e;
++ }
++ }
++ $response->setHttpResponseCode(500);
++ $response->setHeader('Content-Type', 'text/plain');
++ $response->setBody($this->buildContentFromException($exception));
++ $response->sendResponse();
++ return true;
++ }
++ return false;
++ }
++
++ /**
++ * Build content based on an exception
++ *
++ * @param \Exception $exception
++ * @return string
++ */
++ private function buildContentFromException(\Exception $exception) :string
++ {
++ /** @var \Exception[] $exceptions */
++ $exceptions = [];
++
++ do {
++ $exceptions[] = $exception;
++ } while ($exception = $exception->getPrevious());
++
++ $buffer = sprintf("%d exception(s):\n", count($exceptions));
++
++ foreach ($exceptions as $index => $exception) {
++ $buffer .= sprintf(
++ "Exception #%d (%s): %s\n",
++ $index,
++ get_class($exception),
++ $exception->getMessage()
++ );
++ }
++
++ foreach ($exceptions as $index => $exception) {
++ $buffer .= sprintf(
++ "\nException #%d (%s): %s\n%s\n",
++ $index,
++ get_class($exception),
++ $exception->getMessage(),
++ Debug::trace(
++ $exception->getTrace(),
++ true,
++ true,
++ (bool)getenv('MAGE_DEBUG_SHOW_ARGS')
++ )
++ );
++ }
++
++ return $buffer;
++ }
++
++ /**
++ * Handler for bootstrap errors
++ *
++ * @param Bootstrap $bootstrap
++ * @param \Exception &$exception
++ * @param ResponseHttp $response
++ * @return bool
++ */
++ private function handleBootstrapErrors(
++ Bootstrap $bootstrap,
++ \Exception &$exception,
++ ResponseHttp $response
++ ): bool {
++ $bootstrapCode = $bootstrap->getErrorCode();
++ if (Bootstrap::ERR_MAINTENANCE == $bootstrapCode) {
++ require $this->filesystem
++ ->getDirectoryRead(DirectoryList::PUB)
++ ->getAbsolutePath('errors/503.php');
++ return true;
++ }
++ if (Bootstrap::ERR_IS_INSTALLED == $bootstrapCode) {
++ try {
++ $this->redirectToSetup($bootstrap, $exception, $response);
++ return true;
++ } catch (\Exception $e) {
++ $exception = $e;
++ }
++ }
++ return false;
++ }
++
++ /**
++ * Handler for session errors
++ *
++ * @param \Exception $exception
++ * @param ResponseHttp $response
++ * @param RequestHttp $request
++ * @return bool
++ */
++ private function handleSessionException(
++ \Exception $exception,
++ ResponseHttp $response,
++ RequestHttp $request
++ ): bool {
++ if ($exception instanceof SessionException) {
++ $response->setRedirect($request->getDistroBaseUrl());
++ $response->sendHeaders();
++ return true;
++ }
++ return false;
++ }
++
++ /**
++ * Handler for application initialization errors
++ *
++ * @param \Exception $exception
++ * @return bool
++ */
++ private function handleInitException(\Exception $exception) : bool
++ {
++ if ($exception instanceof InitException) {
++ $this->logger->critical($exception);
++ require $this->filesystem
++ ->getDirectoryRead(DirectoryList::PUB)
++ ->getAbsolutePath('errors/404.php');
++ return true;
++ }
++ return false;
++ }
++
++ /**
++ * Handle for any other errors
++ *
++ * @param Bootstrap $bootstrap
++ * @param \Exception $exception
++ * @return bool
++ */
++ private function handleGenericReport(Bootstrap $bootstrap, \Exception $exception): bool
++ {
++ $reportData = [
++ $exception->getMessage(),
++ Debug::trace(
++ $exception->getTrace(),
++ true,
++ false,
++ (bool)getenv('MAGE_DEBUG_SHOW_ARGS')
++ )
++ ];
++ $params = $bootstrap->getParams();
++ if (isset($params['REQUEST_URI'])) {
++ $reportData['url'] = $params['REQUEST_URI'];
++ }
++ if (isset($params['SCRIPT_NAME'])) {
++ $reportData['script_name'] = $params['SCRIPT_NAME'];
++ }
++ $reportData['report_id'] = $this->encryptor->getHash(implode('', $reportData));
++ $this->logger->critical($exception, ['report_id' => $reportData['report_id']]);
++ require $this->filesystem
++ ->getDirectoryRead(DirectoryList::PUB)
++ ->getAbsolutePath('errors/report.php');
++ return true;
++ }
++ /**
++ * If not installed, try to redirect to installation wizard
++ *
++ * @param Bootstrap $bootstrap
++ * @param \Exception $exception
++ * @param ResponseHttp $response
++ * @return void
++ * @throws \Exception
++ */
++ private function redirectToSetup(Bootstrap $bootstrap, \Exception $exception, ResponseHttp $response)
++ {
++ $setupInfo = new SetupInfo($bootstrap->getParams());
++ $projectRoot = $this->filesystem->getDirectoryRead(DirectoryList::ROOT)->getAbsolutePath();
++ if ($setupInfo->isAvailable()) {
++ $response->setRedirect($setupInfo->getUrl());
++ $response->sendHeaders();
++ } else {
++ $newMessage = $exception->getMessage() . "\nNOTE: You cannot install Magento using the Setup Wizard "
++ . "because the Magento setup directory cannot be accessed. \n"
++ . 'You can install Magento using either the command line or you must restore access '
++ . 'to the following directory: ' . $setupInfo->getDir($projectRoot) . "\n";
++
++ throw new \Exception($newMessage, 0, $exception);
++ }
++ }
++}
+diff -Naur a/vendor/magento/framework/App/ExceptionHandlerInterface.php b/vendor/magento/framework/App/ExceptionHandlerInterface.php
+--- /dev/null
++++ b/vendor/magento/framework/App/ExceptionHandlerInterface.php
+@@ -0,0 +1,31 @@
++_objectManager = $objectManager;
+ $this->_eventManager = $eventManager;
+@@ -101,30 +97,15 @@ class Http implements \Magento\Framework\AppInterface
+ $this->_response = $response;
+ $this->_configLoader = $configLoader;
+ $this->_state = $state;
+- $this->_filesystem = $filesystem;
+ $this->registry = $registry;
+- }
+-
+- /**
+- * Add new dependency
+- *
+- * @return \Psr\Log\LoggerInterface
+- *
+- * @deprecated 100.1.0
+- */
+- private function getLogger()
+- {
+- if (!$this->logger instanceof \Psr\Log\LoggerInterface) {
+- $this->logger = \Magento\Framework\App\ObjectManager::getInstance()->get(\Psr\Log\LoggerInterface::class);
+- }
+- return $this->logger;
++ $this->exceptionHandler = $exceptionHandler ?: $this->_objectManager->get(ExceptionHandlerInterface::class);
+ }
+
+ /**
+ * Run application
+ *
+- * @throws \InvalidArgumentException
+ * @return ResponseInterface
++ * @throws \InvalidArgumentException|LocalizedException
+ */
+ public function launch()
+ {
+@@ -152,190 +133,8 @@ class Http implements \Magento\Framework\AppInterface
+ /**
+ * @inheritdoc
+ */
+- public function catchException(Bootstrap $bootstrap, \Exception $exception)
+- {
+- $result = $this->handleDeveloperMode($bootstrap, $exception)
+- || $this->handleBootstrapErrors($bootstrap, $exception)
+- || $this->handleSessionException($exception)
+- || $this->handleInitException($exception)
+- || $this->handleGenericReport($bootstrap, $exception);
+- return $result;
+- }
+-
+- /**
+- * Error handler for developer mode
+- *
+- * @param Bootstrap $bootstrap
+- * @param \Exception $exception
+- * @return bool
+- */
+- private function handleDeveloperMode(Bootstrap $bootstrap, \Exception $exception)
+- {
+- if ($bootstrap->isDeveloperMode()) {
+- if (Bootstrap::ERR_IS_INSTALLED == $bootstrap->getErrorCode()) {
+- try {
+- $this->redirectToSetup($bootstrap, $exception);
+- return true;
+- } catch (\Exception $e) {
+- $exception = $e;
+- }
+- }
+- $this->_response->setHttpResponseCode(500);
+- $this->_response->setHeader('Content-Type', 'text/plain');
+- $this->_response->setBody($this->buildContentFromException($exception));
+- $this->_response->sendResponse();
+- return true;
+- }
+- return false;
+- }
+-
+- /**
+- * Build content based on an exception
+- *
+- * @param \Exception $exception
+- * @return string
+- */
+- private function buildContentFromException(\Exception $exception)
+- {
+- /** @var \Exception[] $exceptions */
+- $exceptions = [];
+-
+- do {
+- $exceptions[] = $exception;
+- } while ($exception = $exception->getPrevious());
+-
+- $buffer = sprintf("%d exception(s):\n", count($exceptions));
+-
+- foreach ($exceptions as $index => $exception) {
+- $buffer .= sprintf("Exception #%d (%s): %s\n", $index, get_class($exception), $exception->getMessage());
+- }
+-
+- foreach ($exceptions as $index => $exception) {
+- $buffer .= sprintf(
+- "\nException #%d (%s): %s\n%s\n",
+- $index,
+- get_class($exception),
+- $exception->getMessage(),
+- Debug::trace(
+- $exception->getTrace(),
+- true,
+- true,
+- (bool)getenv('MAGE_DEBUG_SHOW_ARGS')
+- )
+- );
+- }
+-
+- return $buffer;
+- }
+-
+- /**
+- * If not installed, try to redirect to installation wizard
+- *
+- * @param Bootstrap $bootstrap
+- * @param \Exception $exception
+- * @return void
+- * @throws \Exception
+- */
+- private function redirectToSetup(Bootstrap $bootstrap, \Exception $exception)
+- {
+- $setupInfo = new SetupInfo($bootstrap->getParams());
+- $projectRoot = $this->_filesystem->getDirectoryRead(DirectoryList::ROOT)->getAbsolutePath();
+- if ($setupInfo->isAvailable()) {
+- $this->_response->setRedirect($setupInfo->getUrl());
+- $this->_response->sendHeaders();
+- } else {
+- $newMessage = $exception->getMessage() . "\nNOTE: You cannot install Magento using the Setup Wizard "
+- . "because the Magento setup directory cannot be accessed. \n"
+- . 'You can install Magento using either the command line or you must restore access '
+- . 'to the following directory: ' . $setupInfo->getDir($projectRoot) . "\n";
+-
+- throw new \Exception($newMessage, 0, $exception);
+- }
+- }
+-
+- /**
+- * Handler for bootstrap errors
+- *
+- * @param Bootstrap $bootstrap
+- * @param \Exception &$exception
+- * @return bool
+- */
+- private function handleBootstrapErrors(Bootstrap $bootstrap, \Exception &$exception)
++ public function catchException(Bootstrap $bootstrap, \Exception $exception): bool
+ {
+- $bootstrapCode = $bootstrap->getErrorCode();
+- if (Bootstrap::ERR_MAINTENANCE == $bootstrapCode) {
+- require $this->_filesystem->getDirectoryRead(DirectoryList::PUB)->getAbsolutePath('errors/503.php');
+- return true;
+- }
+- if (Bootstrap::ERR_IS_INSTALLED == $bootstrapCode) {
+- try {
+- $this->redirectToSetup($bootstrap, $exception);
+- return true;
+- } catch (\Exception $e) {
+- $exception = $e;
+- }
+- }
+- return false;
+- }
+-
+- /**
+- * Handler for session errors
+- *
+- * @param \Exception $exception
+- * @return bool
+- */
+- private function handleSessionException(\Exception $exception)
+- {
+- if ($exception instanceof \Magento\Framework\Exception\SessionException) {
+- $this->_response->setRedirect($this->_request->getDistroBaseUrl());
+- $this->_response->sendHeaders();
+- return true;
+- }
+- return false;
+- }
+-
+- /**
+- * Handler for application initialization errors
+- *
+- * @param \Exception $exception
+- * @return bool
+- */
+- private function handleInitException(\Exception $exception)
+- {
+- if ($exception instanceof \Magento\Framework\Exception\State\InitException) {
+- $this->getLogger()->critical($exception);
+- require $this->_filesystem->getDirectoryRead(DirectoryList::PUB)->getAbsolutePath('errors/404.php');
+- return true;
+- }
+- return false;
+- }
+-
+- /**
+- * Handle for any other errors
+- *
+- * @param Bootstrap $bootstrap
+- * @param \Exception $exception
+- * @return bool
+- */
+- private function handleGenericReport(Bootstrap $bootstrap, \Exception $exception)
+- {
+- $reportData = [
+- $exception->getMessage(),
+- Debug::trace(
+- $exception->getTrace(),
+- true,
+- true,
+- (bool)getenv('MAGE_DEBUG_SHOW_ARGS')
+- )
+- ];
+- $params = $bootstrap->getParams();
+- if (isset($params['REQUEST_URI'])) {
+- $reportData['url'] = $params['REQUEST_URI'];
+- }
+- if (isset($params['SCRIPT_NAME'])) {
+- $reportData['script_name'] = $params['SCRIPT_NAME'];
+- }
+- require $this->_filesystem->getDirectoryRead(DirectoryList::PUB)->getAbsolutePath('errors/report.php');
+- return true;
++ return $this->exceptionHandler->handle($bootstrap, $exception, $this->_response, $this->_request);
+ }
+ }
+diff -Naur a/pub/errors/local.xml.sample b/pub/errors/local.xml.sample
+--- a/pub/errors/local.xml.sample
++++ b/pub/errors/local.xml.sample
+@@ -27,5 +27,22 @@
+ value "delete" is for cleaning
+ -->
+ leave
++
++ 0
+
+
+diff -Naur a/pub/errors/processor.php b/pub/errors/processor.php
+--- a/pub/errors/processor.php
++++ b/pub/errors/processor.php
+@@ -6,11 +6,15 @@
+ namespace Magento\Framework\Error;
+
+ use Magento\Framework\Serialize\Serializer\Json;
++use Magento\Framework\Escaper;
++use Magento\Framework\App\ObjectManager;
++use Magento\Framework\App\Response\Http;
+
+ /**
+ * Error processor
+ *
+ * @SuppressWarnings(PHPMD.TooManyFields)
++ * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
+ */
+ class Processor
+ {
+@@ -18,6 +22,7 @@ class Processor
+ const MAGE_ERRORS_DESIGN_XML = 'design.xml';
+ const DEFAULT_SKIN = 'default';
+ const ERROR_DIR = 'pub/errors';
++ const NUMBER_SYMBOLS_IN_SUBDIR_NAME = 2;
+
+ /**
+ * Page title
+@@ -64,7 +69,7 @@ class Processor
+ /**
+ * Report ID
+ *
+- * @var int
++ * @var string
+ */
+ public $reportId;
+
+@@ -125,7 +130,7 @@ class Processor
+ /**
+ * Http response
+ *
+- * @var \Magento\Framework\App\Response\Http
++ * @var Http
+ */
+ protected $_response;
+
+@@ -137,15 +142,25 @@ class Processor
+ private $serializer;
+
+ /**
+- * @param \Magento\Framework\App\Response\Http $response
++ * @var Escaper
++ */
++ private $escaper;
++
++ /**
++ * @param Http $response
+ * @param Json $serializer
++ * @param Escaper $escaper
+ */
+- public function __construct(\Magento\Framework\App\Response\Http $response, Json $serializer = null)
+- {
++ public function __construct(
++ Http $response,
++ Json $serializer = null,
++ Escaper $escaper = null
++ ) {
+ $this->_response = $response;
+ $this->_errorDir = __DIR__ . '/';
+ $this->_reportDir = dirname(dirname($this->_errorDir)) . '/var/report/';
+- $this->serializer = $serializer ?: \Magento\Framework\App\ObjectManager::getInstance()->get(Json::class);
++ $this->serializer = $serializer ?: ObjectManager::getInstance()->get(Json::class);
++ $this->escaper = $escaper ?: ObjectManager::getInstance()->get(Escaper::class);
+
+ if (!empty($_SERVER['SCRIPT_NAME'])) {
+ if (in_array(basename($_SERVER['SCRIPT_NAME'], '.php'), ['404', '503', 'report'])) {
+@@ -155,11 +170,6 @@ class Processor
+ }
+ }
+
+- $reportId = (isset($_GET['id'])) ? (int)$_GET['id'] : null;
+- if ($reportId) {
+- $this->loadReport($reportId);
+- }
+-
+ $this->_indexDir = $this->_getIndexDir();
+ $this->_root = is_dir($this->_indexDir . 'app');
+
+@@ -167,6 +177,10 @@ class Processor
+ if (isset($_GET['skin'])) {
+ $this->_setSkin($_GET['skin']);
+ }
++
++ if (isset($_GET['id'])) {
++ $this->loadReport($_GET['id']);
++ }
+ }
+
+ /**
+@@ -356,6 +370,9 @@ class Processor
+ if ((string)$local->report->trash) {
+ $config->trash = $local->report->trash;
+ }
++ if ($local->report->dir_nesting_level) {
++ $config->dir_nesting_level = (int)$local->report->dir_nesting_level;
++ }
+ if ((string)$local->skin) {
+ $this->_setSkin((string)$local->skin, $config);
+ }
+@@ -452,7 +469,7 @@ class Processor
+ $this->reportData['url'] = $this->getHostUrl() . $reportData['url'];
+ }
+
+- if ($this->reportData['script_name']) {
++ if (isset($this->reportData['script_name'])) {
+ $this->_scriptName = $this->reportData['script_name'];
+ }
+ }
+@@ -463,16 +480,18 @@ class Processor
+ * @param array $reportData
+ * @return string
+ */
+- public function saveReport($reportData)
++ public function saveReport(array $reportData): string
+ {
+- $this->reportData = $reportData;
+- $this->reportId = abs((int)microtime(true) * random_int(100, 1000));
+- $this->_reportFile = $this->_reportDir . '/' . $this->reportId;
+- $this->_setReportData($reportData);
+-
+- if (!file_exists($this->_reportDir)) {
+- @mkdir($this->_reportDir, 0777, true);
++ $this->reportId = $reportData['report_id'];
++ $this->_reportFile = $this->getReportPath(
++ $this->getReportDirNestingLevel($this->reportId),
++ $this->reportId
++ );
++ $reportDirName = dirname($this->_reportFile);
++ if (!file_exists($reportDirName)) {
++ @mkdir($reportDirName, 0777, true);
+ }
++ $this->_setReportData($reportData);
+
+ @file_put_contents($this->_reportFile, $this->serializer->serialize($reportData));
+
+@@ -487,20 +506,118 @@ class Processor
+ /**
+ * Get report
+ *
+- * @param int $reportId
++ * @param string $reportId
+ * @return void
+- * @SuppressWarnings(PHPMD.ExitExpression)
+ */
+ public function loadReport($reportId)
+ {
+- $this->reportId = $reportId;
+- $this->_reportFile = $this->_reportDir . '/' . $reportId;
++ try {
++ if (!$this->isReportIdValid($reportId)) {
++ throw new \RuntimeException("Report Id is invalid");
++ }
++ $reportFile = $this->findReportFile($reportId);
++ if (!is_readable($reportFile)) {
++ throw new \RuntimeException("Report file cannot be read");
++ }
++ $this->reportId = $reportId;
++ $this->_reportFile = $reportFile;
++ $this->_setReportData($this->serializer->unserialize(file_get_contents($this->_reportFile)));
++ } catch (\RuntimeException $e) {
++ $this->redirectToBaseUrl();
++ }
++ }
++
++ /**
++ * Searches for the report file and returns the path to it
++ *
++ * @param string $reportId
++ * @return string
++ * @throws \RuntimeException
++ */
++ private function findReportFile(string $reportId): string
++ {
++ $reportFile = $this->getReportPath(
++ $this->getReportDirNestingLevel($reportId),
++ $reportId
++ );
++ if (file_exists($reportFile)) {
++ return $reportFile;
++ }
++ $maxReportDirNestingLevel = $this->getMaxReportDirNestingLevel($reportId);
++ for ($i = 0; $i <= $maxReportDirNestingLevel; $i++) {
++ $reportFile = $this->getReportPath($i, $reportId);
++ if (file_exists($reportFile)) {
++ return $reportFile;
++ }
++ }
++ throw new \RuntimeException("Report file not found");
++ }
+
+- if (!file_exists($this->_reportFile) || !is_readable($this->_reportFile)) {
+- header("Location: " . $this->getBaseUrl());
+- die();
++ /**
++ * Redirect to a base url
++ * @return void
++ * @SuppressWarnings(PHPMD.ExitExpression)
++ */
++ private function redirectToBaseUrl()
++ {
++ header("Location: " . $this->getBaseUrl());
++ die();
++ }
++
++ /**
++ * Checks report id
++ *
++ * @param string $reportId
++ * @return bool
++ */
++ private function isReportIdValid(string $reportId): bool
++ {
++ return (bool)preg_match('/[a-fA-F0-9]{64}/', $reportId);
++ }
++
++ /**
++ * Returns nesting Level for the report files
++ *
++ * @var $reportId
++ * @return int
++ */
++ private function getReportDirNestingLevel(string $reportId): int
++ {
++ $envName = 'MAGE_ERROR_REPORT_DIR_NESTING_LEVEL';
++ $value = $_ENV[$envName] ?? getenv($envName);
++ if (false === $value && property_exists($this->_config, 'dir_nesting_level')) {
++ $value = $this->_config->dir_nesting_level;
++ }
++ $value = (int)$value;
++ $maxValue = $this->getMaxReportDirNestingLevel($reportId);
++ return 0 < $value && $maxValue >= $value ? $value : 0;
++ }
++
++ /**
++ * Returns maximum nesting level directories of report files
++ *
++ * @param string $reportId
++ * @return integer
++ */
++ private function getMaxReportDirNestingLevel(string $reportId): int
++ {
++ return (int)floor(strlen($reportId) / self::NUMBER_SYMBOLS_IN_SUBDIR_NAME);
++ }
++
++ /**
++ * Get path to reports
++ *
++ * @param integer $reportDirNestingLevel
++ * @param string $reportId
++ * @return string
++ */
++ private function getReportPath(int $reportDirNestingLevel, string $reportId): string
++ {
++ $reportDirPath = $this->_reportDir;
++ for ($i = 0, $j = 0; $j < $reportDirNestingLevel; $i += 2, $j++) {
++ $reportDirPath .= $reportId[$i] . $reportId[$i + 1] . '/';
+ }
+- $this->_setReportData($this->serializer->unserialize(file_get_contents($this->_reportFile)));
++ return $reportDirPath . $reportId;
+ }
+
+ /**
+@@ -514,11 +631,16 @@ class Processor
+ {
+ $this->pageTitle = 'Error Submission Form';
+
+- $this->postData['firstName'] = (isset($_POST['firstname'])) ? trim(htmlspecialchars($_POST['firstname'])) : '';
+- $this->postData['lastName'] = (isset($_POST['lastname'])) ? trim(htmlspecialchars($_POST['lastname'])) : '';
+- $this->postData['email'] = (isset($_POST['email'])) ? trim(htmlspecialchars($_POST['email'])) : '';
+- $this->postData['telephone'] = (isset($_POST['telephone'])) ? trim(htmlspecialchars($_POST['telephone'])) : '';
+- $this->postData['comment'] = (isset($_POST['comment'])) ? trim(htmlspecialchars($_POST['comment'])) : '';
++ $this->postData['firstName'] = (isset($_POST['firstname']))
++ ? trim($this->escaper->escapeHtml($_POST['firstname'])) : '';
++ $this->postData['lastName'] = (isset($_POST['lastname']))
++ ? trim($this->escaper->escapeHtml($_POST['lastname'])) : '';
++ $this->postData['email'] = (isset($_POST['email']))
++ ? trim($this->escaper->escapeHtml($_POST['email'])) : '';
++ $this->postData['telephone'] = (isset($_POST['telephone']))
++ ? trim($this->escaper->escapeHtml($_POST['telephone'])) : '';
++ $this->postData['comment'] = (isset($_POST['comment']))
++ ? trim($this->escaper->escapeHtml($_POST['comment'])) : '';
+
+ if (isset($_POST['submit'])) {
+ if ($this->_validate()) {
diff --git a/patches/MAGECLOUD-3392__reduce_q-ty_of_error_report_files__2.3.0.patch b/patches/MAGECLOUD-3392__reduce_q-ty_of_error_report_files__2.3.0.patch
new file mode 100644
index 0000000..37cbf15
--- /dev/null
+++ b/patches/MAGECLOUD-3392__reduce_q-ty_of_error_report_files__2.3.0.patch
@@ -0,0 +1,1049 @@
+diff -Naur a/app/etc/di.xml b/app/etc/di.xml
+--- a/app/etc/di.xml
++++ b/app/etc/di.xml
+@@ -46,6 +46,7 @@
+
+
+
++
+
+
+ system/currency/installed
+diff -Naur a/vendor/magento/framework/App/ExceptionHandler.php b/vendor/magento/framework/App/ExceptionHandler.php
+--- /dev/null
++++ b/vendor/magento/framework/App/ExceptionHandler.php
+@@ -0,0 +1,284 @@
++encryptor = $encryptor;
++ $this->filesystem = $filesystem;
++ $this->logger = $logger;
++ }
++
++ /**
++ * Handles exception of HTTP web application
++ *
++ * @param Bootstrap $bootstrap
++ * @param \Exception $exception
++ * @param ResponseHttp $response
++ * @param RequestHttp $request
++ * @return bool
++ */
++ public function handle(
++ Bootstrap $bootstrap,
++ \Exception $exception,
++ ResponseHttp $response,
++ RequestHttp $request
++ ): bool {
++ $result = $this->handleDeveloperMode($bootstrap, $exception, $response)
++ || $this->handleBootstrapErrors($bootstrap, $exception, $response)
++ || $this->handleSessionException($exception, $response, $request)
++ || $this->handleInitException($exception)
++ || $this->handleGenericReport($bootstrap, $exception);
++ return $result;
++ }
++
++ /**
++ * Error handler for developer mode
++ *
++ * @param Bootstrap $bootstrap
++ * @param \Exception $exception
++ * @param ResponseHttp $response
++ * @return bool
++ */
++ private function handleDeveloperMode(
++ Bootstrap $bootstrap,
++ \Exception $exception,
++ ResponseHttp $response
++ ): bool {
++ if ($bootstrap->isDeveloperMode()) {
++ if (Bootstrap::ERR_IS_INSTALLED == $bootstrap->getErrorCode()) {
++ try {
++ $this->redirectToSetup($bootstrap, $exception, $response);
++ return true;
++ } catch (\Exception $e) {
++ $exception = $e;
++ }
++ }
++ $response->setHttpResponseCode(500);
++ $response->setHeader('Content-Type', 'text/plain');
++ $response->setBody($this->buildContentFromException($exception));
++ $response->sendResponse();
++ return true;
++ }
++ return false;
++ }
++
++ /**
++ * Build content based on an exception
++ *
++ * @param \Exception $exception
++ * @return string
++ */
++ private function buildContentFromException(\Exception $exception): string
++ {
++ /** @var \Exception[] $exceptions */
++ $exceptions = [];
++
++ do {
++ $exceptions[] = $exception;
++ } while ($exception = $exception->getPrevious());
++
++ $buffer = sprintf("%d exception(s):\n", count($exceptions));
++
++ foreach ($exceptions as $index => $exception) {
++ $buffer .= sprintf(
++ "Exception #%d (%s): %s\n",
++ $index,
++ get_class($exception),
++ $exception->getMessage()
++ );
++ }
++
++ foreach ($exceptions as $index => $exception) {
++ $buffer .= sprintf(
++ "\nException #%d (%s): %s\n%s\n",
++ $index,
++ get_class($exception),
++ $exception->getMessage(),
++ Debug::trace(
++ $exception->getTrace(),
++ true,
++ true,
++ (bool)getenv('MAGE_DEBUG_SHOW_ARGS')
++ )
++ );
++ }
++
++ return $buffer;
++ }
++
++ /**
++ * Handler for bootstrap errors
++ *
++ * @param Bootstrap $bootstrap
++ * @param \Exception $exception
++ * @param ResponseHttp $response
++ * @return bool
++ */
++ private function handleBootstrapErrors(
++ Bootstrap $bootstrap,
++ \Exception &$exception,
++ ResponseHttp $response
++ ): bool {
++ $bootstrapCode = $bootstrap->getErrorCode();
++ if (Bootstrap::ERR_MAINTENANCE == $bootstrapCode) {
++ // phpcs:ignore Magento2.Security.IncludeFile
++ require $this->filesystem
++ ->getDirectoryRead(DirectoryList::PUB)
++ ->getAbsolutePath('errors/503.php');
++ return true;
++ }
++ if (Bootstrap::ERR_IS_INSTALLED == $bootstrapCode) {
++ try {
++ $this->redirectToSetup($bootstrap, $exception, $response);
++ return true;
++ } catch (\Exception $e) {
++ $exception = $e;
++ }
++ }
++ return false;
++ }
++
++ /**
++ * Handler for session errors
++ *
++ * @param \Exception $exception
++ * @param ResponseHttp $response
++ * @param RequestHttp $request
++ * @return bool
++ */
++ private function handleSessionException(
++ \Exception $exception,
++ ResponseHttp $response,
++ RequestHttp $request
++ ): bool {
++ if ($exception instanceof SessionException) {
++ $response->setRedirect($request->getDistroBaseUrl());
++ $response->sendHeaders();
++ return true;
++ }
++ return false;
++ }
++
++ /**
++ * Handler for application initialization errors
++ *
++ * @param \Exception $exception
++ * @return bool
++ */
++ private function handleInitException(\Exception $exception): bool
++ {
++ if ($exception instanceof InitException) {
++ $this->logger->critical($exception);
++ // phpcs:ignore Magento2.Security.IncludeFile
++ require $this->filesystem
++ ->getDirectoryRead(DirectoryList::PUB)
++ ->getAbsolutePath('errors/404.php');
++ return true;
++ }
++ return false;
++ }
++
++ /**
++ * Handle for any other errors
++ *
++ * @param Bootstrap $bootstrap
++ * @param \Exception $exception
++ * @return bool
++ */
++ private function handleGenericReport(Bootstrap $bootstrap, \Exception $exception): bool
++ {
++ $reportData = [
++ $exception->getMessage(),
++ Debug::trace(
++ $exception->getTrace(),
++ true,
++ false,
++ (bool)getenv('MAGE_DEBUG_SHOW_ARGS')
++ )
++ ];
++ $params = $bootstrap->getParams();
++ if (isset($params['REQUEST_URI'])) {
++ $reportData['url'] = $params['REQUEST_URI'];
++ }
++ if (isset($params['SCRIPT_NAME'])) {
++ $reportData['script_name'] = $params['SCRIPT_NAME'];
++ }
++ $reportData['report_id'] = $this->encryptor->getHash(implode('', $reportData));
++ $this->logger->critical($exception, ['report_id' => $reportData['report_id']]);
++ // phpcs:ignore Magento2.Security.IncludeFile
++ require $this->filesystem
++ ->getDirectoryRead(DirectoryList::PUB)
++ ->getAbsolutePath('errors/report.php');
++ return true;
++ }
++
++ /**
++ * If not installed, try to redirect to installation wizard
++ *
++ * @param Bootstrap $bootstrap
++ * @param \Exception $exception
++ * @param ResponseHttp $response
++ * @return void
++ * @throws \Exception
++ */
++ private function redirectToSetup(Bootstrap $bootstrap, \Exception $exception, ResponseHttp $response)
++ {
++ $setupInfo = new SetupInfo($bootstrap->getParams());
++ $projectRoot = $this->filesystem->getDirectoryRead(DirectoryList::ROOT)->getAbsolutePath();
++ if ($setupInfo->isAvailable()) {
++ $response->setRedirect($setupInfo->getUrl());
++ $response->sendHeaders();
++ } else {
++ $newMessage = $exception->getMessage() . "\nNOTE: You cannot install Magento using the Setup Wizard "
++ . "because the Magento setup directory cannot be accessed. \n"
++ . 'You can install Magento using either the command line or you must restore access '
++ . 'to the following directory: ' . $setupInfo->getDir($projectRoot) . "\n";
++ // phpcs:ignore Magento2.Exceptions.DirectThrow
++ throw new \Exception($newMessage, 0, $exception);
++ }
++ }
++}
+diff -Naur a/vendor/magento/framework/App/ExceptionHandlerInterface.php b/vendor/magento/framework/App/ExceptionHandlerInterface.php
+--- /dev/null
++++ b/vendor/magento/framework/App/ExceptionHandlerInterface.php
+@@ -0,0 +1,31 @@
++_objectManager = $objectManager;
+ $this->_eventManager = $eventManager;
+@@ -100,30 +96,15 @@ class Http implements \Magento\Framework\AppInterface
+ $this->_response = $response;
+ $this->_configLoader = $configLoader;
+ $this->_state = $state;
+- $this->_filesystem = $filesystem;
+ $this->registry = $registry;
+- }
+-
+- /**
+- * Add new dependency
+- *
+- * @return \Psr\Log\LoggerInterface
+- *
+- * @deprecated 100.1.0
+- */
+- private function getLogger()
+- {
+- if (!$this->logger instanceof \Psr\Log\LoggerInterface) {
+- $this->logger = \Magento\Framework\App\ObjectManager::getInstance()->get(\Psr\Log\LoggerInterface::class);
+- }
+- return $this->logger;
++ $this->exceptionHandler = $exceptionHandler ?: $this->_objectManager->get(ExceptionHandlerInterface::class);
+ }
+
+ /**
+ * Run application
+ *
+- * @throws \InvalidArgumentException
+ * @return ResponseInterface
++ * @throws LocalizedException|\InvalidArgumentException
+ */
+ public function launch()
+ {
+@@ -142,6 +123,9 @@ class Http implements \Magento\Framework\AppInterface
+ } else {
+ throw new \InvalidArgumentException('Invalid return type');
+ }
++ if ($this->_request->isHead() && $this->_response->getHttpResponseCode() == 200) {
++ $this->handleHeadRequest();
++ }
+ // This event gives possibility to launch something before sending output (allow cookie setting)
+ $eventParams = ['request' => $this->_request, 'response' => $this->_response];
+ $this->_eventManager->dispatch('controller_front_send_response_before', $eventParams);
+@@ -149,178 +133,26 @@ class Http implements \Magento\Framework\AppInterface
+ }
+
+ /**
+- * {@inheritdoc}
+- */
+- public function catchException(Bootstrap $bootstrap, \Exception $exception)
+- {
+- $result = $this->handleDeveloperMode($bootstrap, $exception)
+- || $this->handleBootstrapErrors($bootstrap, $exception)
+- || $this->handleSessionException($exception)
+- || $this->handleInitException($exception)
+- || $this->handleGenericReport($bootstrap, $exception);
+- return $result;
+- }
+-
+- /**
+- * Error handler for developer mode
+- *
+- * @param Bootstrap $bootstrap
+- * @param \Exception $exception
+- * @return bool
+- */
+- private function handleDeveloperMode(Bootstrap $bootstrap, \Exception $exception)
+- {
+- if ($bootstrap->isDeveloperMode()) {
+- if (Bootstrap::ERR_IS_INSTALLED == $bootstrap->getErrorCode()) {
+- try {
+- $this->redirectToSetup($bootstrap, $exception);
+- return true;
+- } catch (\Exception $e) {
+- $exception = $e;
+- }
+- }
+- $this->_response->setHttpResponseCode(500);
+- $this->_response->setHeader('Content-Type', 'text/plain');
+- $this->_response->setBody($this->buildContentFromException($exception));
+- $this->_response->sendResponse();
+- return true;
+- }
+- return false;
+- }
+-
+- /**
+- * Build content based on an exception
+- *
+- * @param \Exception $exception
+- * @return string
+- */
+- private function buildContentFromException(\Exception $exception)
+- {
+- /** @var \Exception[] $exceptions */
+- $exceptions = [];
+- do {
+- $exceptions[] = $exception;
+- } while ($exception = $exception->getPrevious());
+-
+- $buffer = sprintf("%d exception(s):\n", count($exceptions));
+-
+- foreach ($exceptions as $index => $exception) {
+- $buffer .= sprintf("Exception #%d (%s): %s\n", $index, get_class($exception), $exception->getMessage());
+- }
+-
+- foreach ($exceptions as $index => $exception) {
+- $buffer .= sprintf(
+- "\nException #%d (%s): %s\n%s\n",
+- $index,
+- get_class($exception),
+- $exception->getMessage(),
+- $exception->getTraceAsString()
+- );
+- }
+-
+- return $buffer;
+- }
+-
+- /**
+- * If not installed, try to redirect to installation wizard
++ * Handle HEAD requests by adding the Content-Length header and removing the body from the response.
+ *
+- * @param Bootstrap $bootstrap
+- * @param \Exception $exception
+ * @return void
+- * @throws \Exception
+ */
+- private function redirectToSetup(Bootstrap $bootstrap, \Exception $exception)
++ private function handleHeadRequest()
+ {
+- $setupInfo = new SetupInfo($bootstrap->getParams());
+- $projectRoot = $this->_filesystem->getDirectoryRead(DirectoryList::ROOT)->getAbsolutePath();
+- if ($setupInfo->isAvailable()) {
+- $this->_response->setRedirect($setupInfo->getUrl());
+- $this->_response->sendHeaders();
+- } else {
+- $newMessage = $exception->getMessage() . "\nNOTE: You cannot install Magento using the Setup Wizard "
+- . "because the Magento setup directory cannot be accessed. \n"
+- . 'You can install Magento using either the command line or you must restore access '
+- . 'to the following directory: ' . $setupInfo->getDir($projectRoot) . "\n";
+-
+- throw new \Exception($newMessage, 0, $exception);
+- }
++ // It is possible that some PHP installations have overloaded strlen to use mb_strlen instead.
++ // This means strlen might return the actual number of characters in a non-ascii string instead
++ // of the number of bytes. Use mb_strlen explicitly with a single byte character encoding to ensure
++ // that the content length is calculated in bytes.
++ $contentLength = mb_strlen($this->_response->getContent(), '8bit');
++ $this->_response->clearBody();
++ $this->_response->setHeader('Content-Length', $contentLength);
+ }
+
+ /**
+- * Handler for bootstrap errors
+- *
+- * @param Bootstrap $bootstrap
+- * @param \Exception &$exception
+- * @return bool
+- */
+- private function handleBootstrapErrors(Bootstrap $bootstrap, \Exception &$exception)
+- {
+- $bootstrapCode = $bootstrap->getErrorCode();
+- if (Bootstrap::ERR_MAINTENANCE == $bootstrapCode) {
+- require $this->_filesystem->getDirectoryRead(DirectoryList::PUB)->getAbsolutePath('errors/503.php');
+- return true;
+- }
+- if (Bootstrap::ERR_IS_INSTALLED == $bootstrapCode) {
+- try {
+- $this->redirectToSetup($bootstrap, $exception);
+- return true;
+- } catch (\Exception $e) {
+- $exception = $e;
+- }
+- }
+- return false;
+- }
+-
+- /**
+- * Handler for session errors
+- *
+- * @param \Exception $exception
+- * @return bool
++ * @inheritdoc
+ */
+- private function handleSessionException(\Exception $exception)
++ public function catchException(Bootstrap $bootstrap, \Exception $exception): bool
+ {
+- if ($exception instanceof \Magento\Framework\Exception\SessionException) {
+- $this->_response->setRedirect($this->_request->getDistroBaseUrl());
+- $this->_response->sendHeaders();
+- return true;
+- }
+- return false;
+- }
+-
+- /**
+- * Handler for application initialization errors
+- *
+- * @param \Exception $exception
+- * @return bool
+- */
+- private function handleInitException(\Exception $exception)
+- {
+- if ($exception instanceof \Magento\Framework\Exception\State\InitException) {
+- $this->getLogger()->critical($exception);
+- require $this->_filesystem->getDirectoryRead(DirectoryList::PUB)->getAbsolutePath('errors/404.php');
+- return true;
+- }
+- return false;
+- }
+-
+- /**
+- * Handle for any other errors
+- *
+- * @param Bootstrap $bootstrap
+- * @param \Exception $exception
+- * @return bool
+- */
+- private function handleGenericReport(Bootstrap $bootstrap, \Exception $exception)
+- {
+- $reportData = [$exception->getMessage(), $exception->getTraceAsString()];
+- $params = $bootstrap->getParams();
+- if (isset($params['REQUEST_URI'])) {
+- $reportData['url'] = $params['REQUEST_URI'];
+- }
+- if (isset($params['SCRIPT_NAME'])) {
+- $reportData['script_name'] = $params['SCRIPT_NAME'];
+- }
+- require $this->_filesystem->getDirectoryRead(DirectoryList::PUB)->getAbsolutePath('errors/report.php');
+- return true;
++ return $this->exceptionHandler->handle($bootstrap, $exception, $this->_response, $this->_request);
+ }
+ }
+diff -Naur a/pub/errors/local.xml.sample b/pub/errors/local.xml.sample
+--- a/pub/errors/local.xml.sample
++++ b/pub/errors/local.xml.sample
+@@ -27,5 +27,22 @@
+ value "delete" is for cleaning
+ -->
+ leave
++
++ 0
+
+
+diff -Naur a/pub/errors/processor.php b/pub/errors/processor.php
+--- a/pub/errors/processor.php
++++ b/pub/errors/processor.php
+@@ -3,14 +3,21 @@
+ * Copyright © Magento, Inc. All rights reserved.
+ * See COPYING.txt for license details.
+ */
++declare(strict_types=1);
++
+ namespace Magento\Framework\Error;
+
+ use Magento\Framework\Serialize\Serializer\Json;
++use Magento\Framework\Escaper;
++use Magento\Framework\App\ObjectManager;
++use Magento\Framework\App\Response\Http;
+
+ /**
+ * Error processor
+ *
+ * @SuppressWarnings(PHPMD.TooManyFields)
++ * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
++ * phpcs:ignoreFile
+ */
+ class Processor
+ {
+@@ -18,6 +25,7 @@ class Processor
+ const MAGE_ERRORS_DESIGN_XML = 'design.xml';
+ const DEFAULT_SKIN = 'default';
+ const ERROR_DIR = 'pub/errors';
++ const NUMBER_SYMBOLS_IN_SUBDIR_NAME = 2;
+
+ /**
+ * Page title
+@@ -64,7 +72,7 @@ class Processor
+ /**
+ * Report ID
+ *
+- * @var int
++ * @var string
+ */
+ public $reportId;
+
+@@ -125,7 +133,7 @@ class Processor
+ /**
+ * Http response
+ *
+- * @var \Magento\Framework\App\Response\Http
++ * @var Http
+ */
+ protected $_response;
+
+@@ -137,15 +145,25 @@ class Processor
+ private $serializer;
+
+ /**
+- * @param \Magento\Framework\App\Response\Http $response
++ * @var Escaper
++ */
++ private $escaper;
++
++ /**
++ * @param Http $response
+ * @param Json $serializer
++ * @param Escaper $escaper
+ */
+- public function __construct(\Magento\Framework\App\Response\Http $response, Json $serializer = null)
+- {
++ public function __construct(
++ Http $response,
++ Json $serializer = null,
++ Escaper $escaper = null
++ ) {
+ $this->_response = $response;
+ $this->_errorDir = __DIR__ . '/';
+ $this->_reportDir = dirname(dirname($this->_errorDir)) . '/var/report/';
+- $this->serializer = $serializer ?: \Magento\Framework\App\ObjectManager::getInstance()->get(Json::class);
++ $this->serializer = $serializer ?: ObjectManager::getInstance()->get(Json::class);
++ $this->escaper = $escaper ?: ObjectManager::getInstance()->get(Escaper::class);
+
+ if (!empty($_SERVER['SCRIPT_NAME'])) {
+ if (in_array(basename($_SERVER['SCRIPT_NAME'], '.php'), ['404', '503', 'report'])) {
+@@ -155,11 +173,6 @@ class Processor
+ }
+ }
+
+- $reportId = (isset($_GET['id'])) ? (int)$_GET['id'] : null;
+- if ($reportId) {
+- $this->loadReport($reportId);
+- }
+-
+ $this->_indexDir = $this->_getIndexDir();
+ $this->_root = is_dir($this->_indexDir . 'app');
+
+@@ -167,6 +180,9 @@ class Processor
+ if (isset($_GET['skin'])) {
+ $this->_setSkin($_GET['skin']);
+ }
++ if (isset($_GET['id'])) {
++ $this->loadReport($_GET['id']);
++ }
+ }
+
+ /**
+@@ -257,25 +273,38 @@ class Processor
+ /**
+ * Define server http host
+ */
+- if (!empty($_SERVER['HTTP_HOST'])) {
+- $host = $_SERVER['HTTP_HOST'];
+- } elseif (!empty($_SERVER['SERVER_NAME'])) {
+- $host = $_SERVER['SERVER_NAME'];
+- } else {
+- $host = 'localhost';
+- }
++ $host = $this->resolveHostName();
+
+- $isSecure = (!empty($_SERVER['HTTPS'])) && ($_SERVER['HTTPS'] != 'off');
++ $isSecure = (!empty($_SERVER['HTTPS'])) && ($_SERVER['HTTPS'] !== 'off')
++ || isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && ($_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https');
+ $url = ($isSecure ? 'https://' : 'http://') . $host;
+
+- if (!empty($_SERVER['SERVER_PORT']) && !in_array($_SERVER['SERVER_PORT'], [80, 443])
++ $port = explode(':', $host);
++ if (isset($port[1]) && !in_array($port[1], [80, 443])
+ && !preg_match('/.*?\:[0-9]+$/', $url)
+ ) {
+- $url .= ':' . $_SERVER['SERVER_PORT'];
++ $url .= ':' . $port[1];
+ }
+ return $url;
+ }
+
++ /**
++ * Resolve hostname
++ *
++ * @return string
++ */
++ private function resolveHostName() : string
++ {
++ if (!empty($_SERVER['HTTP_HOST'])) {
++ $host = $_SERVER['HTTP_HOST'];
++ } elseif (!empty($_SERVER['SERVER_NAME'])) {
++ $host = $_SERVER['SERVER_NAME'];
++ } else {
++ $host = 'localhost';
++ }
++ return $host;
++ }
++
+ /**
+ * Retrieve base URL
+ *
+@@ -355,6 +384,9 @@ class Processor
+ if ((string)$local->report->trash) {
+ $config->trash = $local->report->trash;
+ }
++ if ($local->report->dir_nesting_level) {
++ $config->dir_nesting_level = (int)$local->report->dir_nesting_level;
++ }
+ if ((string)$local->skin) {
+ $this->_setSkin((string)$local->skin, $config);
+ }
+@@ -379,6 +411,8 @@ class Processor
+ }
+
+ /**
++ * Render page
++ *
+ * @param string $template
+ * @return string
+ */
+@@ -449,7 +483,7 @@ class Processor
+ $this->reportData['url'] = $this->getHostUrl() . $reportData['url'];
+ }
+
+- if ($this->reportData['script_name']) {
++ if (isset($this->reportData['script_name'])) {
+ $this->_scriptName = $this->reportData['script_name'];
+ }
+ }
+@@ -460,18 +494,20 @@ class Processor
+ * @param array $reportData
+ * @return string
+ */
+- public function saveReport($reportData)
++ public function saveReport(array $reportData): string
+ {
+- $this->reportData = $reportData;
+- $this->reportId = abs(intval(microtime(true) * random_int(100, 1000)));
+- $this->_reportFile = $this->_reportDir . '/' . $this->reportId;
+- $this->_setReportData($reportData);
+-
+- if (!file_exists($this->_reportDir)) {
+- @mkdir($this->_reportDir, 0777, true);
++ $this->reportId = $reportData['report_id'];
++ $this->_reportFile = $this->getReportPath(
++ $this->getReportDirNestingLevel($this->reportId),
++ $this->reportId
++ );
++ $reportDirName = dirname($this->_reportFile);
++ if (!file_exists($reportDirName)) {
++ @mkdir($reportDirName, 0777, true);
+ }
++ $this->_setReportData($reportData);
+
+- @file_put_contents($this->_reportFile, $this->serializer->serialize($reportData));
++ @file_put_contents($this->_reportFile, $this->serializer->serialize($reportData). PHP_EOL);
+
+ if (isset($reportData['skin']) && self::DEFAULT_SKIN != $reportData['skin']) {
+ $this->_setSkin($reportData['skin']);
+@@ -484,20 +520,117 @@ class Processor
+ /**
+ * Get report
+ *
+- * @param int $reportId
++ * @param string $reportId
+ * @return void
+- * @SuppressWarnings(PHPMD.ExitExpression)
+ */
+ public function loadReport($reportId)
+ {
+- $this->reportId = $reportId;
+- $this->_reportFile = $this->_reportDir . '/' . $reportId;
++ try {
++ if (!$this->isReportIdValid($reportId)) {
++ throw new \RuntimeException("Report Id is invalid");
++ }
++ $reportFile = $this->findReportFile($reportId);
++ if (!is_readable($reportFile)) {
++ throw new \RuntimeException("Report file cannot be read");
++ }
++ $this->reportId = $reportId;
++ $this->_reportFile = $reportFile;
++ $this->_setReportData($this->serializer->unserialize(file_get_contents($this->_reportFile)));
++ } catch (\RuntimeException $e) {
++ $this->redirectToBaseUrl();
++ }
++ }
+
+- if (!file_exists($this->_reportFile) || !is_readable($this->_reportFile)) {
+- header("Location: " . $this->getBaseUrl());
+- die();
++ /**
++ * Searches for the report file and returns the path to it
++ *
++ * @param string $reportId
++ * @return string
++ * @throws \RuntimeException
++ */
++ private function findReportFile(string $reportId): string
++ {
++ $reportFile = $this->getReportPath(
++ $this->getReportDirNestingLevel($reportId),
++ $reportId
++ );
++ if (file_exists($reportFile)) {
++ return $reportFile;
+ }
+- $this->_setReportData($this->serializer->unserialize(file_get_contents($this->_reportFile)));
++ $maxReportDirNestingLevel = $this->getMaxReportDirNestingLevel($reportId);
++ for ($i = 0; $i <= $maxReportDirNestingLevel; $i++) {
++ $reportFile = $this->getReportPath($i, $reportId);
++ if (file_exists($reportFile)) {
++ return $reportFile;
++ }
++ }
++ throw new \RuntimeException("Report file not found");
++ }
++
++ /**
++ * Redirect to a base url
++ * @return void
++ */
++ private function redirectToBaseUrl()
++ {
++ header("Location: " . $this->getBaseUrl());
++ die();
++ }
++
++ /**
++ * Checks report id
++ *
++ * @param string $reportId
++ * @return bool
++ */
++ private function isReportIdValid(string $reportId): bool
++ {
++ return (bool)preg_match('/[a-fA-F0-9]{64}/', $reportId);
++ }
++
++ /**
++ * Get path to reports
++ *
++ * @param integer $reportDirNestingLevel
++ * @param string $reportId
++ * @return string
++ */
++ private function getReportPath(int $reportDirNestingLevel, string $reportId): string
++ {
++ $reportDirPath = $this->_reportDir;
++ for ($i = 0, $j = 0; $j < $reportDirNestingLevel; $i += 2, $j++) {
++ $reportDirPath .= $reportId[$i] . $reportId[$i + 1] . '/';
++ }
++ return $reportDirPath . $reportId;
++ }
++
++ /**
++ * Returns nesting Level for the report files
++ *
++ * @var $reportId
++ * @return int
++ */
++ private function getReportDirNestingLevel(string $reportId): int
++ {
++ $envName = 'MAGE_ERROR_REPORT_DIR_NESTING_LEVEL';
++ $value = $_ENV[$envName] ?? getenv($envName);
++ if(false === $value && property_exists($this->_config, 'dir_nesting_level')) {
++ $value = $this->_config->dir_nesting_level;
++ }
++ $value = (int)$value;
++ $maxValue= $this->getMaxReportDirNestingLevel($reportId);
++ return 0 < $value && $maxValue >= $value ? $value : 0;
++ }
++
++ /**
++ * Returns maximum nesting level directories of report files
++ *
++ * @param string $reportId
++ * @return integer
++ */
++ private function getMaxReportDirNestingLevel(string $reportId): int
++ {
++ return (int)floor(strlen($reportId) / self::NUMBER_SYMBOLS_IN_SUBDIR_NAME);
+ }
+
+ /**
+@@ -511,11 +644,16 @@ class Processor
+ {
+ $this->pageTitle = 'Error Submission Form';
+
+- $this->postData['firstName'] = (isset($_POST['firstname'])) ? trim(htmlspecialchars($_POST['firstname'])) : '';
+- $this->postData['lastName'] = (isset($_POST['lastname'])) ? trim(htmlspecialchars($_POST['lastname'])) : '';
+- $this->postData['email'] = (isset($_POST['email'])) ? trim(htmlspecialchars($_POST['email'])) : '';
+- $this->postData['telephone'] = (isset($_POST['telephone'])) ? trim(htmlspecialchars($_POST['telephone'])) : '';
+- $this->postData['comment'] = (isset($_POST['comment'])) ? trim(htmlspecialchars($_POST['comment'])) : '';
++ $this->postData['firstName'] = (isset($_POST['firstname']))
++ ? trim($this->escaper->escapeHtml($_POST['firstname'])) : '';
++ $this->postData['lastName'] = (isset($_POST['lastname']))
++ ? trim($this->escaper->escapeHtml($_POST['lastname'])) : '';
++ $this->postData['email'] = (isset($_POST['email']))
++ ? trim($this->escaper->escapeHtml($_POST['email'])) : '';
++ $this->postData['telephone'] = (isset($_POST['telephone']))
++ ? trim($this->escaper->escapeHtml($_POST['telephone'])) : '';
++ $this->postData['comment'] = (isset($_POST['comment']))
++ ? trim($this->escaper->escapeHtml($_POST['comment'])) : '';
+
+ if (isset($_POST['submit'])) {
+ if ($this->_validate()) {
diff --git a/patches/MAGECLOUD-3392__reduce_q-ty_of_error_report_files__2.3.1.patch b/patches/MAGECLOUD-3392__reduce_q-ty_of_error_report_files__2.3.1.patch
new file mode 100644
index 0000000..9eedde7
--- /dev/null
+++ b/patches/MAGECLOUD-3392__reduce_q-ty_of_error_report_files__2.3.1.patch
@@ -0,0 +1,1006 @@
+diff -Naur a/app/etc/di.xml b/app/etc/di.xml
+--- a/app/etc/di.xml
++++ b/app/etc/di.xml
+@@ -47,6 +47,7 @@
+
+
+
++
+
+
+ system/currency/installed
+diff -Naur a/vendor/magento/framework/App/ExceptionHandler.php b/vendor/magento/framework/App/ExceptionHandler.php
+--- /dev/null
++++ b/vendor/magento/framework/App/ExceptionHandler.php
+@@ -0,0 +1,284 @@
++encryptor = $encryptor;
++ $this->filesystem = $filesystem;
++ $this->logger = $logger;
++ }
++
++ /**
++ * Handles exception of HTTP web application
++ *
++ * @param Bootstrap $bootstrap
++ * @param \Exception $exception
++ * @param ResponseHttp $response
++ * @param RequestHttp $request
++ * @return bool
++ */
++ public function handle(
++ Bootstrap $bootstrap,
++ \Exception $exception,
++ ResponseHttp $response,
++ RequestHttp $request
++ ): bool {
++ $result = $this->handleDeveloperMode($bootstrap, $exception, $response)
++ || $this->handleBootstrapErrors($bootstrap, $exception, $response)
++ || $this->handleSessionException($exception, $response, $request)
++ || $this->handleInitException($exception)
++ || $this->handleGenericReport($bootstrap, $exception);
++ return $result;
++ }
++
++ /**
++ * Error handler for developer mode
++ *
++ * @param Bootstrap $bootstrap
++ * @param \Exception $exception
++ * @param ResponseHttp $response
++ * @return bool
++ */
++ private function handleDeveloperMode(
++ Bootstrap $bootstrap,
++ \Exception $exception,
++ ResponseHttp $response
++ ): bool {
++ if ($bootstrap->isDeveloperMode()) {
++ if (Bootstrap::ERR_IS_INSTALLED == $bootstrap->getErrorCode()) {
++ try {
++ $this->redirectToSetup($bootstrap, $exception, $response);
++ return true;
++ } catch (\Exception $e) {
++ $exception = $e;
++ }
++ }
++ $response->setHttpResponseCode(500);
++ $response->setHeader('Content-Type', 'text/plain');
++ $response->setBody($this->buildContentFromException($exception));
++ $response->sendResponse();
++ return true;
++ }
++ return false;
++ }
++
++ /**
++ * Build content based on an exception
++ *
++ * @param \Exception $exception
++ * @return string
++ */
++ private function buildContentFromException(\Exception $exception): string
++ {
++ /** @var \Exception[] $exceptions */
++ $exceptions = [];
++
++ do {
++ $exceptions[] = $exception;
++ } while ($exception = $exception->getPrevious());
++
++ $buffer = sprintf("%d exception(s):\n", count($exceptions));
++
++ foreach ($exceptions as $index => $exception) {
++ $buffer .= sprintf(
++ "Exception #%d (%s): %s\n",
++ $index,
++ get_class($exception),
++ $exception->getMessage()
++ );
++ }
++
++ foreach ($exceptions as $index => $exception) {
++ $buffer .= sprintf(
++ "\nException #%d (%s): %s\n%s\n",
++ $index,
++ get_class($exception),
++ $exception->getMessage(),
++ Debug::trace(
++ $exception->getTrace(),
++ true,
++ true,
++ (bool)getenv('MAGE_DEBUG_SHOW_ARGS')
++ )
++ );
++ }
++
++ return $buffer;
++ }
++
++ /**
++ * Handler for bootstrap errors
++ *
++ * @param Bootstrap $bootstrap
++ * @param \Exception $exception
++ * @param ResponseHttp $response
++ * @return bool
++ */
++ private function handleBootstrapErrors(
++ Bootstrap $bootstrap,
++ \Exception &$exception,
++ ResponseHttp $response
++ ): bool {
++ $bootstrapCode = $bootstrap->getErrorCode();
++ if (Bootstrap::ERR_MAINTENANCE == $bootstrapCode) {
++ // phpcs:ignore Magento2.Security.IncludeFile
++ require $this->filesystem
++ ->getDirectoryRead(DirectoryList::PUB)
++ ->getAbsolutePath('errors/503.php');
++ return true;
++ }
++ if (Bootstrap::ERR_IS_INSTALLED == $bootstrapCode) {
++ try {
++ $this->redirectToSetup($bootstrap, $exception, $response);
++ return true;
++ } catch (\Exception $e) {
++ $exception = $e;
++ }
++ }
++ return false;
++ }
++
++ /**
++ * Handler for session errors
++ *
++ * @param \Exception $exception
++ * @param ResponseHttp $response
++ * @param RequestHttp $request
++ * @return bool
++ */
++ private function handleSessionException(
++ \Exception $exception,
++ ResponseHttp $response,
++ RequestHttp $request
++ ): bool {
++ if ($exception instanceof SessionException) {
++ $response->setRedirect($request->getDistroBaseUrl());
++ $response->sendHeaders();
++ return true;
++ }
++ return false;
++ }
++
++ /**
++ * Handler for application initialization errors
++ *
++ * @param \Exception $exception
++ * @return bool
++ */
++ private function handleInitException(\Exception $exception): bool
++ {
++ if ($exception instanceof InitException) {
++ $this->logger->critical($exception);
++ // phpcs:ignore Magento2.Security.IncludeFile
++ require $this->filesystem
++ ->getDirectoryRead(DirectoryList::PUB)
++ ->getAbsolutePath('errors/404.php');
++ return true;
++ }
++ return false;
++ }
++
++ /**
++ * Handle for any other errors
++ *
++ * @param Bootstrap $bootstrap
++ * @param \Exception $exception
++ * @return bool
++ */
++ private function handleGenericReport(Bootstrap $bootstrap, \Exception $exception): bool
++ {
++ $reportData = [
++ $exception->getMessage(),
++ Debug::trace(
++ $exception->getTrace(),
++ true,
++ false,
++ (bool)getenv('MAGE_DEBUG_SHOW_ARGS')
++ )
++ ];
++ $params = $bootstrap->getParams();
++ if (isset($params['REQUEST_URI'])) {
++ $reportData['url'] = $params['REQUEST_URI'];
++ }
++ if (isset($params['SCRIPT_NAME'])) {
++ $reportData['script_name'] = $params['SCRIPT_NAME'];
++ }
++ $reportData['report_id'] = $this->encryptor->getHash(implode('', $reportData));
++ $this->logger->critical($exception, ['report_id' => $reportData['report_id']]);
++ // phpcs:ignore Magento2.Security.IncludeFile
++ require $this->filesystem
++ ->getDirectoryRead(DirectoryList::PUB)
++ ->getAbsolutePath('errors/report.php');
++ return true;
++ }
++
++ /**
++ * If not installed, try to redirect to installation wizard
++ *
++ * @param Bootstrap $bootstrap
++ * @param \Exception $exception
++ * @param ResponseHttp $response
++ * @return void
++ * @throws \Exception
++ */
++ private function redirectToSetup(Bootstrap $bootstrap, \Exception $exception, ResponseHttp $response)
++ {
++ $setupInfo = new SetupInfo($bootstrap->getParams());
++ $projectRoot = $this->filesystem->getDirectoryRead(DirectoryList::ROOT)->getAbsolutePath();
++ if ($setupInfo->isAvailable()) {
++ $response->setRedirect($setupInfo->getUrl());
++ $response->sendHeaders();
++ } else {
++ $newMessage = $exception->getMessage() . "\nNOTE: You cannot install Magento using the Setup Wizard "
++ . "because the Magento setup directory cannot be accessed. \n"
++ . 'You can install Magento using either the command line or you must restore access '
++ . 'to the following directory: ' . $setupInfo->getDir($projectRoot) . "\n";
++ // phpcs:ignore Magento2.Exceptions.DirectThrow
++ throw new \Exception($newMessage, 0, $exception);
++ }
++ }
++}
+diff -Naur a/vendor/magento/framework/App/ExceptionHandlerInterface.php b/vendor/magento/framework/App/ExceptionHandlerInterface.php
+--- /dev/null
++++ b/vendor/magento/framework/App/ExceptionHandlerInterface.php
+@@ -0,0 +1,31 @@
++_objectManager = $objectManager;
+ $this->_eventManager = $eventManager;
+@@ -101,30 +96,15 @@ class Http implements \Magento\Framework\AppInterface
+ $this->_response = $response;
+ $this->_configLoader = $configLoader;
+ $this->_state = $state;
+- $this->_filesystem = $filesystem;
+ $this->registry = $registry;
+- }
+-
+- /**
+- * Add new dependency
+- *
+- * @return \Psr\Log\LoggerInterface
+- *
+- * @deprecated 100.1.0
+- */
+- private function getLogger()
+- {
+- if (!$this->logger instanceof \Psr\Log\LoggerInterface) {
+- $this->logger = \Magento\Framework\App\ObjectManager::getInstance()->get(\Psr\Log\LoggerInterface::class);
+- }
+- return $this->logger;
++ $this->exceptionHandler = $exceptionHandler ?: $this->_objectManager->get(ExceptionHandlerInterface::class);
+ }
+
+ /**
+ * Run application
+ *
+- * @throws \InvalidArgumentException
+ * @return ResponseInterface
++ * @throws LocalizedException|\InvalidArgumentException
+ */
+ public function launch()
+ {
+@@ -143,6 +123,9 @@ class Http implements \Magento\Framework\AppInterface
+ } else {
+ throw new \InvalidArgumentException('Invalid return type');
+ }
++ if ($this->_request->isHead() && $this->_response->getHttpResponseCode() == 200) {
++ $this->handleHeadRequest();
++ }
+ // This event gives possibility to launch something before sending output (allow cookie setting)
+ $eventParams = ['request' => $this->_request, 'response' => $this->_response];
+ $this->_eventManager->dispatch('controller_front_send_response_before', $eventParams);
+@@ -150,192 +133,26 @@ class Http implements \Magento\Framework\AppInterface
+ }
+
+ /**
+- * @inheritdoc
+- */
+- public function catchException(Bootstrap $bootstrap, \Exception $exception)
+- {
+- $result = $this->handleDeveloperMode($bootstrap, $exception)
+- || $this->handleBootstrapErrors($bootstrap, $exception)
+- || $this->handleSessionException($exception)
+- || $this->handleInitException($exception)
+- || $this->handleGenericReport($bootstrap, $exception);
+- return $result;
+- }
+-
+- /**
+- * Error handler for developer mode
+- *
+- * @param Bootstrap $bootstrap
+- * @param \Exception $exception
+- * @return bool
+- */
+- private function handleDeveloperMode(Bootstrap $bootstrap, \Exception $exception)
+- {
+- if ($bootstrap->isDeveloperMode()) {
+- if (Bootstrap::ERR_IS_INSTALLED == $bootstrap->getErrorCode()) {
+- try {
+- $this->redirectToSetup($bootstrap, $exception);
+- return true;
+- } catch (\Exception $e) {
+- $exception = $e;
+- }
+- }
+- $this->_response->setHttpResponseCode(500);
+- $this->_response->setHeader('Content-Type', 'text/plain');
+- $this->_response->setBody($this->buildContentFromException($exception));
+- $this->_response->sendResponse();
+- return true;
+- }
+- return false;
+- }
+-
+- /**
+- * Build content based on an exception
++ * Handle HEAD requests by adding the Content-Length header and removing the body from the response.
+ *
+- * @param \Exception $exception
+- * @return string
+- */
+- private function buildContentFromException(\Exception $exception)
+- {
+- /** @var \Exception[] $exceptions */
+- $exceptions = [];
+-
+- do {
+- $exceptions[] = $exception;
+- } while ($exception = $exception->getPrevious());
+-
+- $buffer = sprintf("%d exception(s):\n", count($exceptions));
+-
+- foreach ($exceptions as $index => $exception) {
+- $buffer .= sprintf("Exception #%d (%s): %s\n", $index, get_class($exception), $exception->getMessage());
+- }
+-
+- foreach ($exceptions as $index => $exception) {
+- $buffer .= sprintf(
+- "\nException #%d (%s): %s\n%s\n",
+- $index,
+- get_class($exception),
+- $exception->getMessage(),
+- Debug::trace(
+- $exception->getTrace(),
+- true,
+- true,
+- (bool)getenv('MAGE_DEBUG_SHOW_ARGS')
+- )
+- );
+- }
+-
+- return $buffer;
+- }
+-
+- /**
+- * If not installed, try to redirect to installation wizard
+- *
+- * @param Bootstrap $bootstrap
+- * @param \Exception $exception
+ * @return void
+- * @throws \Exception
+- */
+- private function redirectToSetup(Bootstrap $bootstrap, \Exception $exception)
+- {
+- $setupInfo = new SetupInfo($bootstrap->getParams());
+- $projectRoot = $this->_filesystem->getDirectoryRead(DirectoryList::ROOT)->getAbsolutePath();
+- if ($setupInfo->isAvailable()) {
+- $this->_response->setRedirect($setupInfo->getUrl());
+- $this->_response->sendHeaders();
+- } else {
+- $newMessage = $exception->getMessage() . "\nNOTE: You cannot install Magento using the Setup Wizard "
+- . "because the Magento setup directory cannot be accessed. \n"
+- . 'You can install Magento using either the command line or you must restore access '
+- . 'to the following directory: ' . $setupInfo->getDir($projectRoot) . "\n";
+-
+- throw new \Exception($newMessage, 0, $exception);
+- }
+- }
+-
+- /**
+- * Handler for bootstrap errors
+- *
+- * @param Bootstrap $bootstrap
+- * @param \Exception &$exception
+- * @return bool
+- */
+- private function handleBootstrapErrors(Bootstrap $bootstrap, \Exception &$exception)
+- {
+- $bootstrapCode = $bootstrap->getErrorCode();
+- if (Bootstrap::ERR_MAINTENANCE == $bootstrapCode) {
+- require $this->_filesystem->getDirectoryRead(DirectoryList::PUB)->getAbsolutePath('errors/503.php');
+- return true;
+- }
+- if (Bootstrap::ERR_IS_INSTALLED == $bootstrapCode) {
+- try {
+- $this->redirectToSetup($bootstrap, $exception);
+- return true;
+- } catch (\Exception $e) {
+- $exception = $e;
+- }
+- }
+- return false;
+- }
+-
+- /**
+- * Handler for session errors
+- *
+- * @param \Exception $exception
+- * @return bool
+ */
+- private function handleSessionException(\Exception $exception)
++ private function handleHeadRequest()
+ {
+- if ($exception instanceof \Magento\Framework\Exception\SessionException) {
+- $this->_response->setRedirect($this->_request->getDistroBaseUrl());
+- $this->_response->sendHeaders();
+- return true;
+- }
+- return false;
++ // It is possible that some PHP installations have overloaded strlen to use mb_strlen instead.
++ // This means strlen might return the actual number of characters in a non-ascii string instead
++ // of the number of bytes. Use mb_strlen explicitly with a single byte character encoding to ensure
++ // that the content length is calculated in bytes.
++ $contentLength = mb_strlen($this->_response->getContent(), '8bit');
++ $this->_response->clearBody();
++ $this->_response->setHeader('Content-Length', $contentLength);
+ }
+
+ /**
+- * Handler for application initialization errors
+- *
+- * @param \Exception $exception
+- * @return bool
+- */
+- private function handleInitException(\Exception $exception)
+- {
+- if ($exception instanceof \Magento\Framework\Exception\State\InitException) {
+- $this->getLogger()->critical($exception);
+- require $this->_filesystem->getDirectoryRead(DirectoryList::PUB)->getAbsolutePath('errors/404.php');
+- return true;
+- }
+- return false;
+- }
+-
+- /**
+- * Handle for any other errors
+- *
+- * @param Bootstrap $bootstrap
+- * @param \Exception $exception
+- * @return bool
++ * @inheritdoc
+ */
+- private function handleGenericReport(Bootstrap $bootstrap, \Exception $exception)
++ public function catchException(Bootstrap $bootstrap, \Exception $exception): bool
+ {
+- $reportData = [
+- $exception->getMessage(),
+- Debug::trace(
+- $exception->getTrace(),
+- true,
+- true,
+- (bool)getenv('MAGE_DEBUG_SHOW_ARGS')
+- )
+- ];
+- $params = $bootstrap->getParams();
+- if (isset($params['REQUEST_URI'])) {
+- $reportData['url'] = $params['REQUEST_URI'];
+- }
+- if (isset($params['SCRIPT_NAME'])) {
+- $reportData['script_name'] = $params['SCRIPT_NAME'];
+- }
+- require $this->_filesystem->getDirectoryRead(DirectoryList::PUB)->getAbsolutePath('errors/report.php');
+- return true;
++ return $this->exceptionHandler->handle($bootstrap, $exception, $this->_response, $this->_request);
+ }
+ }
+diff -Naur a/pub/errors/local.xml.sample b/pub/errors/local.xml.sample
+--- a/pub/errors/local.xml.sample
++++ b/pub/errors/local.xml.sample
+@@ -27,5 +27,22 @@
+ value "delete" is for cleaning
+ -->
+ leave
++
++ 0
+
+
+diff -Naur a/pub/errors/processor.php b/pub/errors/processor.php
+--- a/pub/errors/processor.php
++++ b/pub/errors/processor.php
+@@ -3,14 +3,21 @@
+ * Copyright © Magento, Inc. All rights reserved.
+ * See COPYING.txt for license details.
+ */
++declare(strict_types=1);
++
+ namespace Magento\Framework\Error;
+
+ use Magento\Framework\Serialize\Serializer\Json;
++use Magento\Framework\Escaper;
++use Magento\Framework\App\ObjectManager;
++use Magento\Framework\App\Response\Http;
+
+ /**
+ * Error processor
+ *
+ * @SuppressWarnings(PHPMD.TooManyFields)
++ * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
++ * phpcs:ignoreFile
+ */
+ class Processor
+ {
+@@ -18,6 +25,7 @@ class Processor
+ const MAGE_ERRORS_DESIGN_XML = 'design.xml';
+ const DEFAULT_SKIN = 'default';
+ const ERROR_DIR = 'pub/errors';
++ const NUMBER_SYMBOLS_IN_SUBDIR_NAME = 2;
+
+ /**
+ * Page title
+@@ -64,7 +72,7 @@ class Processor
+ /**
+ * Report ID
+ *
+- * @var int
++ * @var string
+ */
+ public $reportId;
+
+@@ -125,7 +133,7 @@ class Processor
+ /**
+ * Http response
+ *
+- * @var \Magento\Framework\App\Response\Http
++ * @var Http
+ */
+ protected $_response;
+
+@@ -137,15 +145,25 @@ class Processor
+ private $serializer;
+
+ /**
+- * @param \Magento\Framework\App\Response\Http $response
++ * @var Escaper
++ */
++ private $escaper;
++
++ /**
++ * @param Http $response
+ * @param Json $serializer
++ * @param Escaper $escaper
+ */
+- public function __construct(\Magento\Framework\App\Response\Http $response, Json $serializer = null)
+- {
++ public function __construct(
++ Http $response,
++ Json $serializer = null,
++ Escaper $escaper = null
++ ) {
+ $this->_response = $response;
+ $this->_errorDir = __DIR__ . '/';
+ $this->_reportDir = dirname(dirname($this->_errorDir)) . '/var/report/';
+- $this->serializer = $serializer ?: \Magento\Framework\App\ObjectManager::getInstance()->get(Json::class);
++ $this->serializer = $serializer ?: ObjectManager::getInstance()->get(Json::class);
++ $this->escaper = $escaper ?: ObjectManager::getInstance()->get(Escaper::class);
+
+ if (!empty($_SERVER['SCRIPT_NAME'])) {
+ if (in_array(basename($_SERVER['SCRIPT_NAME'], '.php'), ['404', '503', 'report'])) {
+@@ -155,11 +173,6 @@ class Processor
+ }
+ }
+
+- $reportId = (isset($_GET['id'])) ? (int)$_GET['id'] : null;
+- if ($reportId) {
+- $this->loadReport($reportId);
+- }
+-
+ $this->_indexDir = $this->_getIndexDir();
+ $this->_root = is_dir($this->_indexDir . 'app');
+
+@@ -167,6 +180,9 @@ class Processor
+ if (isset($_GET['skin'])) {
+ $this->_setSkin($_GET['skin']);
+ }
++ if (isset($_GET['id'])) {
++ $this->loadReport($_GET['id']);
++ }
+ }
+
+ /**
+@@ -368,6 +384,9 @@ class Processor
+ if ((string)$local->report->trash) {
+ $config->trash = $local->report->trash;
+ }
++ if ($local->report->dir_nesting_level) {
++ $config->dir_nesting_level = (int)$local->report->dir_nesting_level;
++ }
+ if ((string)$local->skin) {
+ $this->_setSkin((string)$local->skin, $config);
+ }
+@@ -464,7 +483,7 @@ class Processor
+ $this->reportData['url'] = $this->getHostUrl() . $reportData['url'];
+ }
+
+- if ($this->reportData['script_name']) {
++ if (isset($this->reportData['script_name'])) {
+ $this->_scriptName = $this->reportData['script_name'];
+ }
+ }
+@@ -475,18 +494,20 @@ class Processor
+ * @param array $reportData
+ * @return string
+ */
+- public function saveReport($reportData)
++ public function saveReport(array $reportData): string
+ {
+- $this->reportData = $reportData;
+- $this->reportId = abs((int)(microtime(true) * random_int(100, 1000)));
+- $this->_reportFile = $this->_reportDir . '/' . $this->reportId;
+- $this->_setReportData($reportData);
+-
+- if (!file_exists($this->_reportDir)) {
+- @mkdir($this->_reportDir, 0777, true);
++ $this->reportId = $reportData['report_id'];
++ $this->_reportFile = $this->getReportPath(
++ $this->getReportDirNestingLevel($this->reportId),
++ $this->reportId
++ );
++ $reportDirName = dirname($this->_reportFile);
++ if (!file_exists($reportDirName)) {
++ @mkdir($reportDirName, 0777, true);
+ }
++ $this->_setReportData($reportData);
+
+- @file_put_contents($this->_reportFile, $this->serializer->serialize($reportData));
++ @file_put_contents($this->_reportFile, $this->serializer->serialize($reportData). PHP_EOL);
+
+ if (isset($reportData['skin']) && self::DEFAULT_SKIN != $reportData['skin']) {
+ $this->_setSkin($reportData['skin']);
+@@ -499,20 +520,117 @@ class Processor
+ /**
+ * Get report
+ *
+- * @param int $reportId
++ * @param string $reportId
+ * @return void
+- * @SuppressWarnings(PHPMD.ExitExpression)
+ */
+ public function loadReport($reportId)
+ {
+- $this->reportId = $reportId;
+- $this->_reportFile = $this->_reportDir . '/' . $reportId;
++ try {
++ if (!$this->isReportIdValid($reportId)) {
++ throw new \RuntimeException("Report Id is invalid");
++ }
++ $reportFile = $this->findReportFile($reportId);
++ if (!is_readable($reportFile)) {
++ throw new \RuntimeException("Report file cannot be read");
++ }
++ $this->reportId = $reportId;
++ $this->_reportFile = $reportFile;
++ $this->_setReportData($this->serializer->unserialize(file_get_contents($this->_reportFile)));
++ } catch (\RuntimeException $e) {
++ $this->redirectToBaseUrl();
++ }
++ }
+
+- if (!file_exists($this->_reportFile) || !is_readable($this->_reportFile)) {
+- header("Location: " . $this->getBaseUrl());
+- die();
++ /**
++ * Searches for the report file and returns the path to it
++ *
++ * @param string $reportId
++ * @return string
++ * @throws \RuntimeException
++ */
++ private function findReportFile(string $reportId): string
++ {
++ $reportFile = $this->getReportPath(
++ $this->getReportDirNestingLevel($reportId),
++ $reportId
++ );
++ if (file_exists($reportFile)) {
++ return $reportFile;
++ }
++ $maxReportDirNestingLevel = $this->getMaxReportDirNestingLevel($reportId);
++ for ($i = 0; $i <= $maxReportDirNestingLevel; $i++) {
++ $reportFile = $this->getReportPath($i, $reportId);
++ if (file_exists($reportFile)) {
++ return $reportFile;
++ }
+ }
+- $this->_setReportData($this->serializer->unserialize(file_get_contents($this->_reportFile)));
++ throw new \RuntimeException("Report file not found");
++ }
++
++ /**
++ * Redirect to a base url
++ * @return void
++ */
++ private function redirectToBaseUrl()
++ {
++ header("Location: " . $this->getBaseUrl());
++ die();
++ }
++
++ /**
++ * Checks report id
++ *
++ * @param string $reportId
++ * @return bool
++ */
++ private function isReportIdValid(string $reportId): bool
++ {
++ return (bool)preg_match('/[a-fA-F0-9]{64}/', $reportId);
++ }
++
++ /**
++ * Get path to reports
++ *
++ * @param integer $reportDirNestingLevel
++ * @param string $reportId
++ * @return string
++ */
++ private function getReportPath(int $reportDirNestingLevel, string $reportId): string
++ {
++ $reportDirPath = $this->_reportDir;
++ for ($i = 0, $j = 0; $j < $reportDirNestingLevel; $i += 2, $j++) {
++ $reportDirPath .= $reportId[$i] . $reportId[$i + 1] . '/';
++ }
++ return $reportDirPath . $reportId;
++ }
++
++ /**
++ * Returns nesting Level for the report files
++ *
++ * @var $reportId
++ * @return int
++ */
++ private function getReportDirNestingLevel(string $reportId): int
++ {
++ $envName = 'MAGE_ERROR_REPORT_DIR_NESTING_LEVEL';
++ $value = $_ENV[$envName] ?? getenv($envName);
++ if(false === $value && property_exists($this->_config, 'dir_nesting_level')) {
++ $value = $this->_config->dir_nesting_level;
++ }
++ $value = (int)$value;
++ $maxValue= $this->getMaxReportDirNestingLevel($reportId);
++ return 0 < $value && $maxValue >= $value ? $value : 0;
++ }
++
++ /**
++ * Returns maximum nesting level directories of report files
++ *
++ * @param string $reportId
++ * @return integer
++ */
++ private function getMaxReportDirNestingLevel(string $reportId): int
++ {
++ return (int)floor(strlen($reportId) / self::NUMBER_SYMBOLS_IN_SUBDIR_NAME);
+ }
+
+ /**
+@@ -526,11 +644,16 @@ class Processor
+ {
+ $this->pageTitle = 'Error Submission Form';
+
+- $this->postData['firstName'] = (isset($_POST['firstname'])) ? trim(htmlspecialchars($_POST['firstname'])) : '';
+- $this->postData['lastName'] = (isset($_POST['lastname'])) ? trim(htmlspecialchars($_POST['lastname'])) : '';
+- $this->postData['email'] = (isset($_POST['email'])) ? trim(htmlspecialchars($_POST['email'])) : '';
+- $this->postData['telephone'] = (isset($_POST['telephone'])) ? trim(htmlspecialchars($_POST['telephone'])) : '';
+- $this->postData['comment'] = (isset($_POST['comment'])) ? trim(htmlspecialchars($_POST['comment'])) : '';
++ $this->postData['firstName'] = (isset($_POST['firstname']))
++ ? trim($this->escaper->escapeHtml($_POST['firstname'])) : '';
++ $this->postData['lastName'] = (isset($_POST['lastname']))
++ ? trim($this->escaper->escapeHtml($_POST['lastname'])) : '';
++ $this->postData['email'] = (isset($_POST['email']))
++ ? trim($this->escaper->escapeHtml($_POST['email'])) : '';
++ $this->postData['telephone'] = (isset($_POST['telephone']))
++ ? trim($this->escaper->escapeHtml($_POST['telephone'])) : '';
++ $this->postData['comment'] = (isset($_POST['comment']))
++ ? trim($this->escaper->escapeHtml($_POST['comment'])) : '';
+
+ if (isset($_POST['submit'])) {
+ if ($this->_validate()) {
diff --git a/patches/MAGECLOUD-3392__reduce_q-ty_of_error_report_files__2.3.2.patch b/patches/MAGECLOUD-3392__reduce_q-ty_of_error_report_files__2.3.2.patch
new file mode 100644
index 0000000..a486cd9
--- /dev/null
+++ b/patches/MAGECLOUD-3392__reduce_q-ty_of_error_report_files__2.3.2.patch
@@ -0,0 +1,982 @@
+diff -Naur a/app/etc/di.xml b/app/etc/di.xml
+--- a/app/etc/di.xml
++++ b/app/etc/di.xml
+@@ -47,6 +47,7 @@
+
+
+
++
+
+
+ system/currency/installed
+diff -Naur a/vendor/magento/framework/App/ExceptionHandler.php b/vendor/magento/framework/App/ExceptionHandler.php
+--- /dev/null
++++ b/vendor/magento/framework/App/ExceptionHandler.php
+@@ -0,0 +1,284 @@
++encryptor = $encryptor;
++ $this->filesystem = $filesystem;
++ $this->logger = $logger;
++ }
++
++ /**
++ * Handles exception of HTTP web application
++ *
++ * @param Bootstrap $bootstrap
++ * @param \Exception $exception
++ * @param ResponseHttp $response
++ * @param RequestHttp $request
++ * @return bool
++ */
++ public function handle(
++ Bootstrap $bootstrap,
++ \Exception $exception,
++ ResponseHttp $response,
++ RequestHttp $request
++ ): bool {
++ $result = $this->handleDeveloperMode($bootstrap, $exception, $response)
++ || $this->handleBootstrapErrors($bootstrap, $exception, $response)
++ || $this->handleSessionException($exception, $response, $request)
++ || $this->handleInitException($exception)
++ || $this->handleGenericReport($bootstrap, $exception);
++ return $result;
++ }
++
++ /**
++ * Error handler for developer mode
++ *
++ * @param Bootstrap $bootstrap
++ * @param \Exception $exception
++ * @param ResponseHttp $response
++ * @return bool
++ */
++ private function handleDeveloperMode(
++ Bootstrap $bootstrap,
++ \Exception $exception,
++ ResponseHttp $response
++ ): bool {
++ if ($bootstrap->isDeveloperMode()) {
++ if (Bootstrap::ERR_IS_INSTALLED == $bootstrap->getErrorCode()) {
++ try {
++ $this->redirectToSetup($bootstrap, $exception, $response);
++ return true;
++ } catch (\Exception $e) {
++ $exception = $e;
++ }
++ }
++ $response->setHttpResponseCode(500);
++ $response->setHeader('Content-Type', 'text/plain');
++ $response->setBody($this->buildContentFromException($exception));
++ $response->sendResponse();
++ return true;
++ }
++ return false;
++ }
++
++ /**
++ * Build content based on an exception
++ *
++ * @param \Exception $exception
++ * @return string
++ */
++ private function buildContentFromException(\Exception $exception): string
++ {
++ /** @var \Exception[] $exceptions */
++ $exceptions = [];
++
++ do {
++ $exceptions[] = $exception;
++ } while ($exception = $exception->getPrevious());
++
++ $buffer = sprintf("%d exception(s):\n", count($exceptions));
++
++ foreach ($exceptions as $index => $exception) {
++ $buffer .= sprintf(
++ "Exception #%d (%s): %s\n",
++ $index,
++ get_class($exception),
++ $exception->getMessage()
++ );
++ }
++
++ foreach ($exceptions as $index => $exception) {
++ $buffer .= sprintf(
++ "\nException #%d (%s): %s\n%s\n",
++ $index,
++ get_class($exception),
++ $exception->getMessage(),
++ Debug::trace(
++ $exception->getTrace(),
++ true,
++ true,
++ (bool)getenv('MAGE_DEBUG_SHOW_ARGS')
++ )
++ );
++ }
++
++ return $buffer;
++ }
++
++ /**
++ * Handler for bootstrap errors
++ *
++ * @param Bootstrap $bootstrap
++ * @param \Exception $exception
++ * @param ResponseHttp $response
++ * @return bool
++ */
++ private function handleBootstrapErrors(
++ Bootstrap $bootstrap,
++ \Exception &$exception,
++ ResponseHttp $response
++ ): bool {
++ $bootstrapCode = $bootstrap->getErrorCode();
++ if (Bootstrap::ERR_MAINTENANCE == $bootstrapCode) {
++ // phpcs:ignore Magento2.Security.IncludeFile
++ require $this->filesystem
++ ->getDirectoryRead(DirectoryList::PUB)
++ ->getAbsolutePath('errors/503.php');
++ return true;
++ }
++ if (Bootstrap::ERR_IS_INSTALLED == $bootstrapCode) {
++ try {
++ $this->redirectToSetup($bootstrap, $exception, $response);
++ return true;
++ } catch (\Exception $e) {
++ $exception = $e;
++ }
++ }
++ return false;
++ }
++
++ /**
++ * Handler for session errors
++ *
++ * @param \Exception $exception
++ * @param ResponseHttp $response
++ * @param RequestHttp $request
++ * @return bool
++ */
++ private function handleSessionException(
++ \Exception $exception,
++ ResponseHttp $response,
++ RequestHttp $request
++ ): bool {
++ if ($exception instanceof SessionException) {
++ $response->setRedirect($request->getDistroBaseUrl());
++ $response->sendHeaders();
++ return true;
++ }
++ return false;
++ }
++
++ /**
++ * Handler for application initialization errors
++ *
++ * @param \Exception $exception
++ * @return bool
++ */
++ private function handleInitException(\Exception $exception): bool
++ {
++ if ($exception instanceof InitException) {
++ $this->logger->critical($exception);
++ // phpcs:ignore Magento2.Security.IncludeFile
++ require $this->filesystem
++ ->getDirectoryRead(DirectoryList::PUB)
++ ->getAbsolutePath('errors/404.php');
++ return true;
++ }
++ return false;
++ }
++
++ /**
++ * Handle for any other errors
++ *
++ * @param Bootstrap $bootstrap
++ * @param \Exception $exception
++ * @return bool
++ */
++ private function handleGenericReport(Bootstrap $bootstrap, \Exception $exception): bool
++ {
++ $reportData = [
++ $exception->getMessage(),
++ Debug::trace(
++ $exception->getTrace(),
++ true,
++ false,
++ (bool)getenv('MAGE_DEBUG_SHOW_ARGS')
++ )
++ ];
++ $params = $bootstrap->getParams();
++ if (isset($params['REQUEST_URI'])) {
++ $reportData['url'] = $params['REQUEST_URI'];
++ }
++ if (isset($params['SCRIPT_NAME'])) {
++ $reportData['script_name'] = $params['SCRIPT_NAME'];
++ }
++ $reportData['report_id'] = $this->encryptor->getHash(implode('', $reportData));
++ $this->logger->critical($exception, ['report_id' => $reportData['report_id']]);
++ // phpcs:ignore Magento2.Security.IncludeFile
++ require $this->filesystem
++ ->getDirectoryRead(DirectoryList::PUB)
++ ->getAbsolutePath('errors/report.php');
++ return true;
++ }
++
++ /**
++ * If not installed, try to redirect to installation wizard
++ *
++ * @param Bootstrap $bootstrap
++ * @param \Exception $exception
++ * @param ResponseHttp $response
++ * @return void
++ * @throws \Exception
++ */
++ private function redirectToSetup(Bootstrap $bootstrap, \Exception $exception, ResponseHttp $response)
++ {
++ $setupInfo = new SetupInfo($bootstrap->getParams());
++ $projectRoot = $this->filesystem->getDirectoryRead(DirectoryList::ROOT)->getAbsolutePath();
++ if ($setupInfo->isAvailable()) {
++ $response->setRedirect($setupInfo->getUrl());
++ $response->sendHeaders();
++ } else {
++ $newMessage = $exception->getMessage() . "\nNOTE: You cannot install Magento using the Setup Wizard "
++ . "because the Magento setup directory cannot be accessed. \n"
++ . 'You can install Magento using either the command line or you must restore access '
++ . 'to the following directory: ' . $setupInfo->getDir($projectRoot) . "\n";
++ // phpcs:ignore Magento2.Exceptions.DirectThrow
++ throw new \Exception($newMessage, 0, $exception);
++ }
++ }
++}
+diff -Naur a/vendor/magento/framework/App/ExceptionHandlerInterface.php b/vendor/magento/framework/App/ExceptionHandlerInterface.php
+--- /dev/null
++++ b/vendor/magento/framework/App/ExceptionHandlerInterface.php
+@@ -0,0 +1,31 @@
++_objectManager = $objectManager;
+ $this->_eventManager = $eventManager;
+@@ -102,30 +96,15 @@ class Http implements \Magento\Framework\AppInterface
+ $this->_response = $response;
+ $this->_configLoader = $configLoader;
+ $this->_state = $state;
+- $this->_filesystem = $filesystem;
+ $this->registry = $registry;
+- }
+-
+- /**
+- * Add new dependency
+- *
+- * @return \Psr\Log\LoggerInterface
+- *
+- * @deprecated 100.1.0
+- */
+- private function getLogger()
+- {
+- if (!$this->logger instanceof \Psr\Log\LoggerInterface) {
+- $this->logger = \Magento\Framework\App\ObjectManager::getInstance()->get(\Psr\Log\LoggerInterface::class);
+- }
+- return $this->logger;
++ $this->exceptionHandler = $exceptionHandler ?: $this->_objectManager->get(ExceptionHandlerInterface::class);
+ }
+
+ /**
+ * Run application
+ *
+- * @throws \InvalidArgumentException
+ * @return ResponseInterface
++ * @throws LocalizedException|\InvalidArgumentException
+ */
+ public function launch()
+ {
+@@ -172,193 +151,8 @@ class Http implements \Magento\Framework\AppInterface
+ /**
+ * @inheritdoc
+ */
+- public function catchException(Bootstrap $bootstrap, \Exception $exception)
+- {
+- $result = $this->handleDeveloperMode($bootstrap, $exception)
+- || $this->handleBootstrapErrors($bootstrap, $exception)
+- || $this->handleSessionException($exception)
+- || $this->handleInitException($exception)
+- || $this->handleGenericReport($bootstrap, $exception);
+- return $result;
+- }
+-
+- /**
+- * Error handler for developer mode
+- *
+- * @param Bootstrap $bootstrap
+- * @param \Exception $exception
+- * @return bool
+- */
+- private function handleDeveloperMode(Bootstrap $bootstrap, \Exception $exception)
++ public function catchException(Bootstrap $bootstrap, \Exception $exception): bool
+ {
+- if ($bootstrap->isDeveloperMode()) {
+- if (Bootstrap::ERR_IS_INSTALLED == $bootstrap->getErrorCode()) {
+- try {
+- $this->redirectToSetup($bootstrap, $exception);
+- return true;
+- } catch (\Exception $e) {
+- $exception = $e;
+- }
+- }
+- $this->_response->setHttpResponseCode(500);
+- $this->_response->setHeader('Content-Type', 'text/plain');
+- $this->_response->setBody($this->buildContentFromException($exception));
+- $this->_response->sendResponse();
+- return true;
+- }
+- return false;
+- }
+-
+- /**
+- * Build content based on an exception
+- *
+- * @param \Exception $exception
+- * @return string
+- */
+- private function buildContentFromException(\Exception $exception)
+- {
+- /** @var \Exception[] $exceptions */
+- $exceptions = [];
+-
+- do {
+- $exceptions[] = $exception;
+- } while ($exception = $exception->getPrevious());
+-
+- $buffer = sprintf("%d exception(s):\n", count($exceptions));
+-
+- foreach ($exceptions as $index => $exception) {
+- $buffer .= sprintf("Exception #%d (%s): %s\n", $index, get_class($exception), $exception->getMessage());
+- }
+-
+- foreach ($exceptions as $index => $exception) {
+- $buffer .= sprintf(
+- "\nException #%d (%s): %s\n%s\n",
+- $index,
+- get_class($exception),
+- $exception->getMessage(),
+- Debug::trace(
+- $exception->getTrace(),
+- true,
+- true,
+- (bool)getenv('MAGE_DEBUG_SHOW_ARGS')
+- )
+- );
+- }
+-
+- return $buffer;
+- }
+-
+- /**
+- * If not installed, try to redirect to installation wizard
+- *
+- * @param Bootstrap $bootstrap
+- * @param \Exception $exception
+- * @return void
+- * @throws \Exception
+- */
+- private function redirectToSetup(Bootstrap $bootstrap, \Exception $exception)
+- {
+- $setupInfo = new SetupInfo($bootstrap->getParams());
+- $projectRoot = $this->_filesystem->getDirectoryRead(DirectoryList::ROOT)->getAbsolutePath();
+- if ($setupInfo->isAvailable()) {
+- $this->_response->setRedirect($setupInfo->getUrl());
+- $this->_response->sendHeaders();
+- } else {
+- $newMessage = $exception->getMessage() . "\nNOTE: You cannot install Magento using the Setup Wizard "
+- . "because the Magento setup directory cannot be accessed. \n"
+- . 'You can install Magento using either the command line or you must restore access '
+- . 'to the following directory: ' . $setupInfo->getDir($projectRoot) . "\n";
+- // phpcs:ignore Magento2.Exceptions.DirectThrow
+- throw new \Exception($newMessage, 0, $exception);
+- }
+- }
+-
+- /**
+- * Handler for bootstrap errors
+- *
+- * @param Bootstrap $bootstrap
+- * @param \Exception $exception
+- * @return bool
+- */
+- private function handleBootstrapErrors(Bootstrap $bootstrap, \Exception &$exception)
+- {
+- $bootstrapCode = $bootstrap->getErrorCode();
+- if (Bootstrap::ERR_MAINTENANCE == $bootstrapCode) {
+- // phpcs:ignore Magento2.Security.IncludeFile
+- require $this->_filesystem->getDirectoryRead(DirectoryList::PUB)->getAbsolutePath('errors/503.php');
+- return true;
+- }
+- if (Bootstrap::ERR_IS_INSTALLED == $bootstrapCode) {
+- try {
+- $this->redirectToSetup($bootstrap, $exception);
+- return true;
+- } catch (\Exception $e) {
+- $exception = $e;
+- }
+- }
+- return false;
+- }
+-
+- /**
+- * Handler for session errors
+- *
+- * @param \Exception $exception
+- * @return bool
+- */
+- private function handleSessionException(\Exception $exception)
+- {
+- if ($exception instanceof \Magento\Framework\Exception\SessionException) {
+- $this->_response->setRedirect($this->_request->getDistroBaseUrl());
+- $this->_response->sendHeaders();
+- return true;
+- }
+- return false;
+- }
+-
+- /**
+- * Handler for application initialization errors
+- *
+- * @param \Exception $exception
+- * @return bool
+- */
+- private function handleInitException(\Exception $exception)
+- {
+- if ($exception instanceof \Magento\Framework\Exception\State\InitException) {
+- $this->getLogger()->critical($exception);
+- // phpcs:ignore Magento2.Security.IncludeFile
+- require $this->_filesystem->getDirectoryRead(DirectoryList::PUB)->getAbsolutePath('errors/404.php');
+- return true;
+- }
+- return false;
+- }
+-
+- /**
+- * Handle for any other errors
+- *
+- * @param Bootstrap $bootstrap
+- * @param \Exception $exception
+- * @return bool
+- */
+- private function handleGenericReport(Bootstrap $bootstrap, \Exception $exception)
+- {
+- $reportData = [
+- $exception->getMessage(),
+- Debug::trace(
+- $exception->getTrace(),
+- true,
+- true,
+- (bool)getenv('MAGE_DEBUG_SHOW_ARGS')
+- )
+- ];
+- $params = $bootstrap->getParams();
+- if (isset($params['REQUEST_URI'])) {
+- $reportData['url'] = $params['REQUEST_URI'];
+- }
+- if (isset($params['SCRIPT_NAME'])) {
+- $reportData['script_name'] = $params['SCRIPT_NAME'];
+- }
+- // phpcs:ignore Magento2.Security.IncludeFile
+- require $this->_filesystem->getDirectoryRead(DirectoryList::PUB)->getAbsolutePath('errors/report.php');
+- return true;
++ return $this->exceptionHandler->handle($bootstrap, $exception, $this->_response, $this->_request);
+ }
+ }
+diff -Naur a/pub/errors/local.xml.sample b/pub/errors/local.xml.sample
+--- a/pub/errors/local.xml.sample
++++ b/pub/errors/local.xml.sample
+@@ -27,5 +27,22 @@
+ value "delete" is for cleaning
+ -->
+ leave
++
++ 0
+
+
+diff -Naur a/pub/errors/processor.php b/pub/errors/processor.php
+--- a/pub/errors/processor.php
++++ b/pub/errors/processor.php
+@@ -8,11 +8,15 @@ declare(strict_types=1);
+ namespace Magento\Framework\Error;
+
+ use Magento\Framework\Serialize\Serializer\Json;
++use Magento\Framework\Escaper;
++use Magento\Framework\App\ObjectManager;
++use Magento\Framework\App\Response\Http;
+
+ /**
+ * Error processor
+ *
+ * @SuppressWarnings(PHPMD.TooManyFields)
++ * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
+ * phpcs:ignoreFile
+ */
+ class Processor
+@@ -21,6 +25,7 @@ class Processor
+ const MAGE_ERRORS_DESIGN_XML = 'design.xml';
+ const DEFAULT_SKIN = 'default';
+ const ERROR_DIR = 'pub/errors';
++ const NUMBER_SYMBOLS_IN_SUBDIR_NAME = 2;
+
+ /**
+ * Page title
+@@ -67,7 +72,7 @@ class Processor
+ /**
+ * Report ID
+ *
+- * @var int
++ * @var string
+ */
+ public $reportId;
+
+@@ -128,7 +133,7 @@ class Processor
+ /**
+ * Http response
+ *
+- * @var \Magento\Framework\App\Response\Http
++ * @var Http
+ */
+ protected $_response;
+
+@@ -140,15 +145,25 @@ class Processor
+ private $serializer;
+
+ /**
+- * @param \Magento\Framework\App\Response\Http $response
++ * @var Escaper
++ */
++ private $escaper;
++
++ /**
++ * @param Http $response
+ * @param Json $serializer
++ * @param Escaper $escaper
+ */
+- public function __construct(\Magento\Framework\App\Response\Http $response, Json $serializer = null)
+- {
++ public function __construct(
++ Http $response,
++ Json $serializer = null,
++ Escaper $escaper = null
++ ) {
+ $this->_response = $response;
+ $this->_errorDir = __DIR__ . '/';
+ $this->_reportDir = dirname(dirname($this->_errorDir)) . '/var/report/';
+- $this->serializer = $serializer ?: \Magento\Framework\App\ObjectManager::getInstance()->get(Json::class);
++ $this->serializer = $serializer ?: ObjectManager::getInstance()->get(Json::class);
++ $this->escaper = $escaper ?: ObjectManager::getInstance()->get(Escaper::class);
+
+ if (!empty($_SERVER['SCRIPT_NAME'])) {
+ if (in_array(basename($_SERVER['SCRIPT_NAME'], '.php'), ['404', '503', 'report'])) {
+@@ -158,11 +173,6 @@ class Processor
+ }
+ }
+
+- $reportId = (isset($_GET['id'])) ? (int)$_GET['id'] : null;
+- if ($reportId) {
+- $this->loadReport($reportId);
+- }
+-
+ $this->_indexDir = $this->_getIndexDir();
+ $this->_root = is_dir($this->_indexDir . 'app');
+
+@@ -170,6 +180,9 @@ class Processor
+ if (isset($_GET['skin'])) {
+ $this->_setSkin($_GET['skin']);
+ }
++ if (isset($_GET['id'])) {
++ $this->loadReport($_GET['id']);
++ }
+ }
+
+ /**
+@@ -371,6 +384,9 @@ class Processor
+ if ((string)$local->report->trash) {
+ $config->trash = $local->report->trash;
+ }
++ if ($local->report->dir_nesting_level) {
++ $config->dir_nesting_level = (int)$local->report->dir_nesting_level;
++ }
+ if ((string)$local->skin) {
+ $this->_setSkin((string)$local->skin, $config);
+ }
+@@ -467,7 +483,7 @@ class Processor
+ $this->reportData['url'] = $this->getHostUrl() . $reportData['url'];
+ }
+
+- if ($this->reportData['script_name']) {
++ if (isset($this->reportData['script_name'])) {
+ $this->_scriptName = $this->reportData['script_name'];
+ }
+ }
+@@ -478,18 +494,20 @@ class Processor
+ * @param array $reportData
+ * @return string
+ */
+- public function saveReport($reportData)
++ public function saveReport(array $reportData): string
+ {
+- $this->reportData = $reportData;
+- $this->reportId = abs((int)(microtime(true) * random_int(100, 1000)));
+- $this->_reportFile = $this->_reportDir . '/' . $this->reportId;
+- $this->_setReportData($reportData);
+-
+- if (!file_exists($this->_reportDir)) {
+- @mkdir($this->_reportDir, 0777, true);
++ $this->reportId = $reportData['report_id'];
++ $this->_reportFile = $this->getReportPath(
++ $this->getReportDirNestingLevel($this->reportId),
++ $this->reportId
++ );
++ $reportDirName = dirname($this->_reportFile);
++ if (!file_exists($reportDirName)) {
++ @mkdir($reportDirName, 0777, true);
+ }
++ $this->_setReportData($reportData);
+
+- @file_put_contents($this->_reportFile, $this->serializer->serialize($reportData));
++ @file_put_contents($this->_reportFile, $this->serializer->serialize($reportData). PHP_EOL);
+
+ if (isset($reportData['skin']) && self::DEFAULT_SKIN != $reportData['skin']) {
+ $this->_setSkin($reportData['skin']);
+@@ -502,19 +520,117 @@ class Processor
+ /**
+ * Get report
+ *
+- * @param int $reportId
++ * @param string $reportId
+ * @return void
+ */
+ public function loadReport($reportId)
+ {
+- $this->reportId = $reportId;
+- $this->_reportFile = $this->_reportDir . '/' . $reportId;
++ try {
++ if (!$this->isReportIdValid($reportId)) {
++ throw new \RuntimeException("Report Id is invalid");
++ }
++ $reportFile = $this->findReportFile($reportId);
++ if (!is_readable($reportFile)) {
++ throw new \RuntimeException("Report file cannot be read");
++ }
++ $this->reportId = $reportId;
++ $this->_reportFile = $reportFile;
++ $this->_setReportData($this->serializer->unserialize(file_get_contents($this->_reportFile)));
++ } catch (\RuntimeException $e) {
++ $this->redirectToBaseUrl();
++ }
++ }
++
++ /**
++ * Searches for the report file and returns the path to it
++ *
++ * @param string $reportId
++ * @return string
++ * @throws \RuntimeException
++ */
++ private function findReportFile(string $reportId): string
++ {
++ $reportFile = $this->getReportPath(
++ $this->getReportDirNestingLevel($reportId),
++ $reportId
++ );
++ if (file_exists($reportFile)) {
++ return $reportFile;
++ }
++ $maxReportDirNestingLevel = $this->getMaxReportDirNestingLevel($reportId);
++ for ($i = 0; $i <= $maxReportDirNestingLevel; $i++) {
++ $reportFile = $this->getReportPath($i, $reportId);
++ if (file_exists($reportFile)) {
++ return $reportFile;
++ }
++ }
++ throw new \RuntimeException("Report file not found");
++ }
++
++ /**
++ * Redirect to a base url
++ * @return void
++ */
++ private function redirectToBaseUrl()
++ {
++ header("Location: " . $this->getBaseUrl());
++ die();
++ }
++
++ /**
++ * Checks report id
++ *
++ * @param string $reportId
++ * @return bool
++ */
++ private function isReportIdValid(string $reportId): bool
++ {
++ return (bool)preg_match('/[a-fA-F0-9]{64}/', $reportId);
++ }
++
++ /**
++ * Get path to reports
++ *
++ * @param integer $reportDirNestingLevel
++ * @param string $reportId
++ * @return string
++ */
++ private function getReportPath(int $reportDirNestingLevel, string $reportId): string
++ {
++ $reportDirPath = $this->_reportDir;
++ for ($i = 0, $j = 0; $j < $reportDirNestingLevel; $i += 2, $j++) {
++ $reportDirPath .= $reportId[$i] . $reportId[$i + 1] . '/';
++ }
++ return $reportDirPath . $reportId;
++ }
+
+- if (!file_exists($this->_reportFile) || !is_readable($this->_reportFile)) {
+- header("Location: " . $this->getBaseUrl());
+- die();
++ /**
++ * Returns nesting Level for the report files
++ *
++ * @var $reportId
++ * @return int
++ */
++ private function getReportDirNestingLevel(string $reportId): int
++ {
++ $envName = 'MAGE_ERROR_REPORT_DIR_NESTING_LEVEL';
++ $value = $_ENV[$envName] ?? getenv($envName);
++ if(false === $value && property_exists($this->_config, 'dir_nesting_level')) {
++ $value = $this->_config->dir_nesting_level;
+ }
+- $this->_setReportData($this->serializer->unserialize(file_get_contents($this->_reportFile)));
++ $value = (int)$value;
++ $maxValue= $this->getMaxReportDirNestingLevel($reportId);
++ return 0 < $value && $maxValue >= $value ? $value : 0;
++ }
++
++ /**
++ * Returns maximum nesting level directories of report files
++ *
++ * @param string $reportId
++ * @return integer
++ */
++ private function getMaxReportDirNestingLevel(string $reportId): int
++ {
++ return (int)floor(strlen($reportId) / self::NUMBER_SYMBOLS_IN_SUBDIR_NAME);
+ }
+
+ /**
+@@ -528,11 +644,16 @@ class Processor
+ {
+ $this->pageTitle = 'Error Submission Form';
+
+- $this->postData['firstName'] = (isset($_POST['firstname'])) ? trim(htmlspecialchars($_POST['firstname'])) : '';
+- $this->postData['lastName'] = (isset($_POST['lastname'])) ? trim(htmlspecialchars($_POST['lastname'])) : '';
+- $this->postData['email'] = (isset($_POST['email'])) ? trim(htmlspecialchars($_POST['email'])) : '';
+- $this->postData['telephone'] = (isset($_POST['telephone'])) ? trim(htmlspecialchars($_POST['telephone'])) : '';
+- $this->postData['comment'] = (isset($_POST['comment'])) ? trim(htmlspecialchars($_POST['comment'])) : '';
++ $this->postData['firstName'] = (isset($_POST['firstname']))
++ ? trim($this->escaper->escapeHtml($_POST['firstname'])) : '';
++ $this->postData['lastName'] = (isset($_POST['lastname']))
++ ? trim($this->escaper->escapeHtml($_POST['lastname'])) : '';
++ $this->postData['email'] = (isset($_POST['email']))
++ ? trim($this->escaper->escapeHtml($_POST['email'])) : '';
++ $this->postData['telephone'] = (isset($_POST['telephone']))
++ ? trim($this->escaper->escapeHtml($_POST['telephone'])) : '';
++ $this->postData['comment'] = (isset($_POST['comment']))
++ ? trim($this->escaper->escapeHtml($_POST['comment'])) : '';
+
+ if (isset($_POST['submit'])) {
+ if ($this->_validate()) {
diff --git a/patches/MAGECLOUD-4407__fix_namespace_vertex_tax__3.2.0.patch b/patches/MAGECLOUD-4407__fix_namespace_vertex_tax__3.2.0.patch
new file mode 100644
index 0000000..85eff36
--- /dev/null
+++ b/patches/MAGECLOUD-4407__fix_namespace_vertex_tax__3.2.0.patch
@@ -0,0 +1,12 @@
+diff -Naur a/vendor/vertex/module-tax/Model/FlexField/Processor/OrderCurrencyGetterProcessor.php b/vendor/vertex/module-tax/Model/FlexField/Processor/OrderCurrencyGetterProcessor.php
+--- a/vendor/vertex/module-tax/Model/FlexField/Processor/OrderCurrencyGetterProcessor.php
++++ b/vendor/vertex/module-tax/Model/FlexField/Processor/OrderCurrencyGetterProcessor.php
+@@ -4,7 +4,7 @@
+ * @author Mediotype https://www.mediotype.com/
+ */
+
+-namespace Vertex\Tax\Model\Flexfield\Processor;
++namespace Vertex\Tax\Model\FlexField\Processor;
+
+ use Magento\Framework\Exception\NoSuchEntityException;
+ use Magento\Quote\Api\CartRepositoryInterface;