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;