processor.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622
  1. <?php
  2. /**
  3. * Copyright © Magento, Inc. All rights reserved.
  4. * See COPYING.txt for license details.
  5. */
  6. namespace Magento\Framework\Error;
  7. use Magento\Framework\Serialize\Serializer\Json;
  8. /**
  9. * Error processor
  10. *
  11. * @SuppressWarnings(PHPMD.TooManyFields)
  12. */
  13. class Processor
  14. {
  15. const MAGE_ERRORS_LOCAL_XML = 'local.xml';
  16. const MAGE_ERRORS_DESIGN_XML = 'design.xml';
  17. const DEFAULT_SKIN = 'default';
  18. const ERROR_DIR = 'pub/errors';
  19. /**
  20. * Page title
  21. *
  22. * @var string
  23. */
  24. public $pageTitle;
  25. /**
  26. * Skin URL
  27. *
  28. * @var string
  29. */
  30. public $skinUrl;
  31. /**
  32. * Base URL
  33. *
  34. * @var string
  35. */
  36. public $baseUrl;
  37. /**
  38. * Post data
  39. *
  40. * @var array
  41. */
  42. public $postData;
  43. /**
  44. * Report data
  45. *
  46. * @var array
  47. */
  48. public $reportData;
  49. /**
  50. * Report action
  51. *
  52. * @var string
  53. */
  54. public $reportAction;
  55. /**
  56. * Report ID
  57. *
  58. * @var int
  59. */
  60. public $reportId;
  61. /**
  62. * Report file
  63. *
  64. * @var string
  65. */
  66. protected $_reportFile;
  67. /**
  68. * Show error message
  69. *
  70. * @var bool
  71. */
  72. public $showErrorMsg;
  73. /**
  74. * Show message after sending email
  75. *
  76. * @var bool
  77. */
  78. public $showSentMsg;
  79. /**
  80. * Show form for sending
  81. *
  82. * @var bool
  83. */
  84. public $showSendForm;
  85. /**
  86. * @var string
  87. */
  88. public $reportUrl;
  89. /**
  90. * Server script name
  91. *
  92. * @var string
  93. */
  94. protected $_scriptName;
  95. /**
  96. * Is root
  97. *
  98. * @var bool
  99. */
  100. protected $_root;
  101. /**
  102. * Internal config object
  103. *
  104. * @var \stdClass
  105. */
  106. protected $_config;
  107. /**
  108. * Http response
  109. *
  110. * @var \Magento\Framework\App\Response\Http
  111. */
  112. protected $_response;
  113. /**
  114. * JSON serializer
  115. *
  116. * @var Json
  117. */
  118. private $serializer;
  119. /**
  120. * @param \Magento\Framework\App\Response\Http $response
  121. * @param Json $serializer
  122. */
  123. public function __construct(\Magento\Framework\App\Response\Http $response, Json $serializer = null)
  124. {
  125. $this->_response = $response;
  126. $this->_errorDir = __DIR__ . '/';
  127. $this->_reportDir = dirname(dirname($this->_errorDir)) . '/var/report/';
  128. $this->serializer = $serializer ?: \Magento\Framework\App\ObjectManager::getInstance()->get(Json::class);
  129. if (!empty($_SERVER['SCRIPT_NAME'])) {
  130. if (in_array(basename($_SERVER['SCRIPT_NAME'], '.php'), ['404', '503', 'report'])) {
  131. $this->_scriptName = dirname($_SERVER['SCRIPT_NAME']);
  132. } else {
  133. $this->_scriptName = $_SERVER['SCRIPT_NAME'];
  134. }
  135. }
  136. $reportId = (isset($_GET['id'])) ? (int)$_GET['id'] : null;
  137. if ($reportId) {
  138. $this->loadReport($reportId);
  139. }
  140. $this->_indexDir = $this->_getIndexDir();
  141. $this->_root = is_dir($this->_indexDir . 'app');
  142. $this->_prepareConfig();
  143. if (isset($_GET['skin'])) {
  144. $this->_setSkin($_GET['skin']);
  145. }
  146. }
  147. /**
  148. * Process no cache error
  149. *
  150. * @return \Magento\Framework\App\Response\Http
  151. */
  152. public function processNoCache()
  153. {
  154. $this->pageTitle = 'Error : cached config data is unavailable';
  155. $this->_response->setBody($this->_renderPage('nocache.phtml'));
  156. return $this->_response;
  157. }
  158. /**
  159. * Process 404 error
  160. *
  161. * @return \Magento\Framework\App\Response\Http
  162. */
  163. public function process404()
  164. {
  165. $this->pageTitle = 'Error 404: Not Found';
  166. $this->_response->setHttpResponseCode(404);
  167. $this->_response->setBody($this->_renderPage('404.phtml'));
  168. return $this->_response;
  169. }
  170. /**
  171. * Process 503 error
  172. *
  173. * @return \Magento\Framework\App\Response\Http
  174. */
  175. public function process503()
  176. {
  177. $this->pageTitle = 'Error 503: Service Unavailable';
  178. $this->_response->setHttpResponseCode(503);
  179. $this->_response->setBody($this->_renderPage('503.phtml'));
  180. return $this->_response;
  181. }
  182. /**
  183. * Process report
  184. *
  185. * @return \Magento\Framework\App\Response\Http
  186. */
  187. public function processReport()
  188. {
  189. $this->pageTitle = 'There has been an error processing your request';
  190. $this->_response->setHttpResponseCode(500);
  191. $this->showErrorMsg = false;
  192. $this->showSentMsg = false;
  193. $this->showSendForm = false;
  194. $this->reportAction = $this->_config->action;
  195. $this->_setReportUrl();
  196. if ($this->reportAction == 'email') {
  197. $this->showSendForm = true;
  198. $this->sendReport();
  199. }
  200. $this->_response->setBody($this->_renderPage('report.phtml'));
  201. return $this->_response;
  202. }
  203. /**
  204. * Retrieve skin URL
  205. *
  206. * @return string
  207. */
  208. public function getViewFileUrl()
  209. {
  210. //The url needs to be updated base on Document root path.
  211. return $this->getBaseUrl() .
  212. str_replace(
  213. str_replace('\\', '/', $this->_indexDir),
  214. '',
  215. str_replace('\\', '/', $this->_errorDir)
  216. ) . $this->_config->skin . '/';
  217. }
  218. /**
  219. * Retrieve base host URL without path
  220. *
  221. * @return string
  222. */
  223. public function getHostUrl()
  224. {
  225. /**
  226. * Define server http host
  227. */
  228. $host = $this->resolveHostName();
  229. $isSecure = (!empty($_SERVER['HTTPS'])) && ($_SERVER['HTTPS'] !== 'off')
  230. || isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && ($_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https');
  231. $url = ($isSecure ? 'https://' : 'http://') . $host;
  232. $port = explode(':', $host);
  233. if (isset($port[1]) && !in_array($port[1], [80, 443])
  234. && !preg_match('/.*?\:[0-9]+$/', $url)
  235. ) {
  236. $url .= ':' . $port[1];
  237. }
  238. return $url;
  239. }
  240. /**
  241. * Resolve hostname
  242. *
  243. * @return string
  244. */
  245. private function resolveHostName() : string
  246. {
  247. if (!empty($_SERVER['HTTP_HOST'])) {
  248. $host = $_SERVER['HTTP_HOST'];
  249. } elseif (!empty($_SERVER['SERVER_NAME'])) {
  250. $host = $_SERVER['SERVER_NAME'];
  251. } else {
  252. $host = 'localhost';
  253. }
  254. return $host;
  255. }
  256. /**
  257. * Retrieve base URL
  258. *
  259. * @param bool $param
  260. * @return string
  261. */
  262. public function getBaseUrl($param = false)
  263. {
  264. $path = $this->_scriptName;
  265. if ($param && !$this->_root) {
  266. $path = dirname($path);
  267. }
  268. $basePath = str_replace('\\', '/', dirname($path));
  269. return $this->getHostUrl() . ('/' == $basePath ? '' : $basePath) . '/';
  270. }
  271. /**
  272. * Retrieve client IP address
  273. *
  274. * @return string
  275. */
  276. protected function _getClientIp()
  277. {
  278. return (isset($_SERVER['REMOTE_ADDR'])) ? $_SERVER['REMOTE_ADDR'] : 'undefined';
  279. }
  280. /**
  281. * Get index dir
  282. *
  283. * @return string
  284. */
  285. protected function _getIndexDir()
  286. {
  287. $documentRoot = '';
  288. if (!empty($_SERVER['DOCUMENT_ROOT'])) {
  289. $documentRoot = rtrim(realpath($_SERVER['DOCUMENT_ROOT']), '/');
  290. }
  291. return dirname($documentRoot . $this->_scriptName) . '/';
  292. }
  293. /**
  294. * Prepare config data
  295. *
  296. * @return void
  297. * @SuppressWarnings(PHPMD.CyclomaticComplexity)
  298. * @SuppressWarnings(PHPMD.NPathComplexity)
  299. */
  300. protected function _prepareConfig()
  301. {
  302. $local = $this->_loadXml(self::MAGE_ERRORS_LOCAL_XML);
  303. $design = $this->_loadXml(self::MAGE_ERRORS_DESIGN_XML);
  304. //initial settings
  305. $config = new \stdClass();
  306. $config->action = '';
  307. $config->subject = 'Store Debug Information';
  308. $config->email_address = '';
  309. $config->trash = 'leave';
  310. $config->skin = self::DEFAULT_SKIN;
  311. //combine xml data to one object
  312. if ($design !== null && (string)$design->skin) {
  313. $this->_setSkin((string)$design->skin, $config);
  314. }
  315. if ($local !== null) {
  316. if ((string)$local->report->action) {
  317. $config->action = $local->report->action;
  318. }
  319. if ((string)$local->report->subject) {
  320. $config->subject = $local->report->subject;
  321. }
  322. if ((string)$local->report->email_address) {
  323. $config->email_address = $local->report->email_address;
  324. }
  325. if ((string)$local->report->trash) {
  326. $config->trash = $local->report->trash;
  327. }
  328. if ((string)$local->skin) {
  329. $this->_setSkin((string)$local->skin, $config);
  330. }
  331. }
  332. if ((string)$config->email_address == '' && (string)$config->action == 'email') {
  333. $config->action = '';
  334. }
  335. $this->_config = $config;
  336. }
  337. /**
  338. * Load xml file
  339. *
  340. * @param string $xmlFile
  341. * @return \SimpleXMLElement
  342. */
  343. protected function _loadXml($xmlFile)
  344. {
  345. $configPath = $this->_getFilePath($xmlFile);
  346. return ($configPath) ? simplexml_load_file($configPath) : null;
  347. }
  348. /**
  349. * Render page
  350. *
  351. * @param string $template
  352. * @return string
  353. */
  354. protected function _renderPage($template)
  355. {
  356. $baseTemplate = $this->_getTemplatePath('page.phtml');
  357. $contentTemplate = $this->_getTemplatePath($template);
  358. $html = '';
  359. if ($baseTemplate && $contentTemplate) {
  360. ob_start();
  361. require_once $baseTemplate;
  362. $html = ob_get_clean();
  363. }
  364. return $html;
  365. }
  366. /**
  367. * Find file path
  368. *
  369. * @param string $file
  370. * @param array $directories
  371. * @return string
  372. */
  373. protected function _getFilePath($file, $directories = null)
  374. {
  375. if ($directories === null) {
  376. $directories[] = $this->_errorDir;
  377. }
  378. foreach ($directories as $directory) {
  379. if (file_exists($directory . $file)) {
  380. return $directory . $file;
  381. }
  382. }
  383. }
  384. /**
  385. * Find template path
  386. *
  387. * @param string $template
  388. * @return string
  389. */
  390. protected function _getTemplatePath($template)
  391. {
  392. $directories[] = $this->_errorDir . $this->_config->skin . '/';
  393. if ($this->_config->skin != self::DEFAULT_SKIN) {
  394. $directories[] = $this->_errorDir . self::DEFAULT_SKIN . '/';
  395. }
  396. return $this->_getFilePath($template, $directories);
  397. }
  398. /**
  399. * Set report data
  400. *
  401. * @param array $reportData
  402. * @return void
  403. */
  404. protected function _setReportData($reportData)
  405. {
  406. $this->reportData = $reportData;
  407. if (!isset($reportData['url'])) {
  408. $this->reportData['url'] = '';
  409. } else {
  410. $this->reportData['url'] = $this->getHostUrl() . $reportData['url'];
  411. }
  412. if ($this->reportData['script_name']) {
  413. $this->_scriptName = $this->reportData['script_name'];
  414. }
  415. }
  416. /**
  417. * Create report
  418. *
  419. * @param array $reportData
  420. * @return string
  421. */
  422. public function saveReport($reportData)
  423. {
  424. $this->reportData = $reportData;
  425. $this->reportId = abs((int)(microtime(true) * random_int(100, 1000)));
  426. $this->_reportFile = $this->_reportDir . '/' . $this->reportId;
  427. $this->_setReportData($reportData);
  428. if (!file_exists($this->_reportDir)) {
  429. @mkdir($this->_reportDir, 0777, true);
  430. }
  431. @file_put_contents($this->_reportFile, $this->serializer->serialize($reportData));
  432. if (isset($reportData['skin']) && self::DEFAULT_SKIN != $reportData['skin']) {
  433. $this->_setSkin($reportData['skin']);
  434. }
  435. $this->_setReportUrl();
  436. return $this->reportUrl;
  437. }
  438. /**
  439. * Get report
  440. *
  441. * @param int $reportId
  442. * @return void
  443. * @SuppressWarnings(PHPMD.ExitExpression)
  444. */
  445. public function loadReport($reportId)
  446. {
  447. $this->reportId = $reportId;
  448. $this->_reportFile = $this->_reportDir . '/' . $reportId;
  449. if (!file_exists($this->_reportFile) || !is_readable($this->_reportFile)) {
  450. header("Location: " . $this->getBaseUrl());
  451. die();
  452. }
  453. $this->_setReportData($this->serializer->unserialize(file_get_contents($this->_reportFile)));
  454. }
  455. /**
  456. * Send report
  457. *
  458. * @return void
  459. * @SuppressWarnings(PHPMD.CyclomaticComplexity)
  460. * @SuppressWarnings(PHPMD.NPathComplexity)
  461. */
  462. public function sendReport()
  463. {
  464. $this->pageTitle = 'Error Submission Form';
  465. $this->postData['firstName'] = (isset($_POST['firstname'])) ? trim(htmlspecialchars($_POST['firstname'])) : '';
  466. $this->postData['lastName'] = (isset($_POST['lastname'])) ? trim(htmlspecialchars($_POST['lastname'])) : '';
  467. $this->postData['email'] = (isset($_POST['email'])) ? trim(htmlspecialchars($_POST['email'])) : '';
  468. $this->postData['telephone'] = (isset($_POST['telephone'])) ? trim(htmlspecialchars($_POST['telephone'])) : '';
  469. $this->postData['comment'] = (isset($_POST['comment'])) ? trim(htmlspecialchars($_POST['comment'])) : '';
  470. if (isset($_POST['submit'])) {
  471. if ($this->_validate()) {
  472. $msg = "URL: {$this->reportData['url']}\n"
  473. . "IP Address: {$this->_getClientIp()}\n"
  474. . "First Name: {$this->postData['firstName']}\n"
  475. . "Last Name: {$this->postData['lastName']}\n"
  476. . "Email Address: {$this->postData['email']}\n";
  477. if ($this->postData['telephone']) {
  478. $msg .= "Telephone: {$this->postData['telephone']}\n";
  479. }
  480. if ($this->postData['comment']) {
  481. $msg .= "Comment: {$this->postData['comment']}\n";
  482. }
  483. $subject = sprintf('%s [%s]', (string)$this->_config->subject, $this->reportId);
  484. @mail((string)$this->_config->email_address, $subject, $msg);
  485. $this->showSendForm = false;
  486. $this->showSentMsg = true;
  487. } else {
  488. $this->showErrorMsg = true;
  489. }
  490. } else {
  491. $time = gmdate('Y-m-d H:i:s \G\M\T');
  492. $msg = "URL: {$this->reportData['url']}\n"
  493. . "IP Address: {$this->_getClientIp()}\n"
  494. . "Time: {$time}\n"
  495. . "Error:\n{$this->reportData[0]}\n\n"
  496. . "Trace:\n{$this->reportData[1]}";
  497. $subject = sprintf('%s [%s]', (string)$this->_config->subject, $this->reportId);
  498. @mail((string)$this->_config->email_address, $subject, $msg);
  499. if ($this->_config->trash == 'delete') {
  500. @unlink($this->_reportFile);
  501. }
  502. }
  503. }
  504. /**
  505. * Validate submitted post data
  506. *
  507. * @return bool
  508. */
  509. protected function _validate()
  510. {
  511. $email = preg_match(
  512. '/^[_a-z0-9-]+(\.[_a-z0-9-]+)*@[a-z0-9-]+(\.[a-z0-9-]+)*(\.[a-z]{2,3})$/',
  513. $this->postData['email']
  514. );
  515. return ($this->postData['firstName'] && $this->postData['lastName'] && $email);
  516. }
  517. /**
  518. * Skin setter
  519. *
  520. * @param string $value
  521. * @param \stdClass $config
  522. * @return void
  523. */
  524. protected function _setSkin($value, \stdClass $config = null)
  525. {
  526. if (preg_match('/^[a-z0-9_]+$/i', $value) && is_dir($this->_errorDir . $value)) {
  527. if (!$config) {
  528. if ($this->_config) {
  529. $config = $this->_config;
  530. }
  531. }
  532. if ($config) {
  533. $config->skin = $value;
  534. }
  535. }
  536. }
  537. /**
  538. * Set current report URL from current params
  539. *
  540. * @return void
  541. */
  542. protected function _setReportUrl()
  543. {
  544. if ($this->reportId && $this->_config && isset($this->_config->skin)) {
  545. $this->reportUrl = "{$this->getBaseUrl(true)}pub/errors/report.php?"
  546. . http_build_query(['id' => $this->reportId, 'skin' => $this->_config->skin]);
  547. }
  548. }
  549. }