ExceptionRendererTest.php 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
  5. * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
  6. *
  7. * Licensed under The MIT License
  8. * For full copyright and license information, please see the LICENSE.txt
  9. * Redistributions of files must retain the above copyright notice
  10. *
  11. * @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
  12. * @link https://cakephp.org CakePHP(tm) Project
  13. * @since 2.0.0
  14. * @license https://opensource.org/licenses/mit-license.php MIT License
  15. */
  16. namespace Cake\Test\TestCase\Error;
  17. use Cake\Controller\Controller;
  18. use Cake\Controller\Exception\InvalidParameterException;
  19. use Cake\Controller\Exception\MissingActionException;
  20. use Cake\Controller\Exception\MissingComponentException;
  21. use Cake\Core\Configure;
  22. use Cake\Core\Exception\CakeException;
  23. use Cake\Core\Exception\MissingPluginException;
  24. use Cake\Datasource\Exception\MissingDatasourceConfigException;
  25. use Cake\Datasource\Exception\MissingDatasourceException;
  26. use Cake\Error\ExceptionRenderer;
  27. use Cake\Event\EventInterface;
  28. use Cake\Event\EventManager;
  29. use Cake\Http\Exception\HttpException;
  30. use Cake\Http\Exception\InternalErrorException;
  31. use Cake\Http\Exception\MethodNotAllowedException;
  32. use Cake\Http\Exception\MissingControllerException;
  33. use Cake\Http\Exception\NotFoundException;
  34. use Cake\Http\ServerRequest;
  35. use Cake\Mailer\Exception\MissingActionException as MissingMailerActionException;
  36. use Cake\ORM\Exception\MissingBehaviorException;
  37. use Cake\Routing\Router;
  38. use Cake\TestSuite\TestCase;
  39. use Cake\View\Exception\MissingHelperException;
  40. use Cake\View\Exception\MissingLayoutException;
  41. use Cake\View\Exception\MissingTemplateException;
  42. use Exception;
  43. use OutOfBoundsException;
  44. use RuntimeException;
  45. use TestApp\Controller\Admin\ErrorController as PrefixErrorController;
  46. use TestApp\Error\Exception\MissingWidgetThing;
  47. use TestApp\Error\Exception\MissingWidgetThingException;
  48. use TestApp\Error\Exception\MyPDOException;
  49. use TestApp\Error\MyCustomExceptionRenderer;
  50. use TestApp\Error\TestAppsExceptionRenderer;
  51. use TestPlugin\Controller\ErrorController as PluginErrorController;
  52. use function Cake\Core\h;
  53. class ExceptionRendererTest extends TestCase
  54. {
  55. /**
  56. * @var bool
  57. */
  58. protected $restoreError = false;
  59. /**
  60. * @var bool
  61. */
  62. protected $called;
  63. /**
  64. * setup create a request object to get out of router later.
  65. */
  66. public function setUp(): void
  67. {
  68. parent::setUp();
  69. Configure::write('Config.language', 'eng');
  70. Router::reload();
  71. $request = new ServerRequest(['base' => '']);
  72. Router::setRequest($request);
  73. Configure::write('debug', true);
  74. }
  75. /**
  76. * tearDown
  77. */
  78. public function tearDown(): void
  79. {
  80. parent::tearDown();
  81. $this->clearPlugins();
  82. if ($this->restoreError) {
  83. restore_error_handler();
  84. }
  85. }
  86. public function testControllerInstanceForPrefixedRequest(): void
  87. {
  88. $this->setAppNamespace('TestApp');
  89. $exception = new NotFoundException('Page not found');
  90. $request = new ServerRequest();
  91. $request = $request
  92. ->withParam('controller', 'Articles')
  93. ->withParam('prefix', 'Admin');
  94. $ExceptionRenderer = new MyCustomExceptionRenderer($exception, $request);
  95. $this->assertInstanceOf(
  96. PrefixErrorController::class,
  97. $ExceptionRenderer->__debugInfo()['controller']
  98. );
  99. }
  100. /**
  101. * Test that prefixed controllers in plugins use the plugin
  102. * error controller if it exists.
  103. *
  104. * @return void
  105. */
  106. public function testControllerInstanceForPluginPrefixedRequest(): void
  107. {
  108. $this->loadPlugins(['TestPlugin']);
  109. $this->setAppNamespace('TestApp');
  110. $exception = new NotFoundException('Page not found');
  111. $request = new ServerRequest();
  112. $request = $request
  113. ->withParam('controller', 'Comments')
  114. ->withParam('plugin', 'TestPlugin')
  115. ->withParam('prefix', 'Admin');
  116. $ExceptionRenderer = new MyCustomExceptionRenderer($exception, $request);
  117. $this->assertInstanceOf(
  118. PluginErrorController::class,
  119. $ExceptionRenderer->__debugInfo()['controller']
  120. );
  121. }
  122. /**
  123. * testTemplatePath
  124. */
  125. public function testTemplatePath(): void
  126. {
  127. $request = (new ServerRequest())
  128. ->withParam('controller', 'Foo')
  129. ->withParam('action', 'bar');
  130. $exception = new NotFoundException();
  131. $ExceptionRenderer = new ExceptionRenderer($exception, $request);
  132. $ExceptionRenderer->render();
  133. $controller = $ExceptionRenderer->__debugInfo()['controller'];
  134. $this->assertSame('error400', $controller->viewBuilder()->getTemplate());
  135. $this->assertSame('Error', $controller->viewBuilder()->getTemplatePath());
  136. $request = $request->withParam('prefix', 'Admin');
  137. $exception = new MissingActionException(['controller' => 'Foo', 'action' => 'bar']);
  138. $ExceptionRenderer = new ExceptionRenderer($exception, $request);
  139. $ExceptionRenderer->render();
  140. $controller = $ExceptionRenderer->__debugInfo()['controller'];
  141. $this->assertSame('missingAction', $controller->viewBuilder()->getTemplate());
  142. $this->assertSame('Error', $controller->viewBuilder()->getTemplatePath());
  143. Configure::write('debug', false);
  144. $ExceptionRenderer = new ExceptionRenderer($exception, $request);
  145. $ExceptionRenderer->render();
  146. $controller = $ExceptionRenderer->__debugInfo()['controller'];
  147. $this->assertSame('error400', $controller->viewBuilder()->getTemplate());
  148. $this->assertSame(
  149. 'Admin' . DIRECTORY_SEPARATOR . 'Error',
  150. $controller->viewBuilder()->getTemplatePath()
  151. );
  152. }
  153. /**
  154. * test that methods declared in an ExceptionRenderer subclass are not converted
  155. * into error400 when debug > 0
  156. */
  157. public function testSubclassMethodsNotBeingConvertedToError(): void
  158. {
  159. $exception = new MissingWidgetThingException('Widget not found');
  160. $ExceptionRenderer = new MyCustomExceptionRenderer($exception);
  161. $result = $ExceptionRenderer->render();
  162. $this->assertSame('widget thing is missing', (string)$result->getBody());
  163. }
  164. /**
  165. * test that subclass methods are not converted when debug = 0
  166. */
  167. public function testSubclassMethodsNotBeingConvertedDebug0(): void
  168. {
  169. Configure::write('debug', false);
  170. $exception = new MissingWidgetThingException('Widget not found');
  171. $ExceptionRenderer = new MyCustomExceptionRenderer($exception);
  172. $result = $ExceptionRenderer->render();
  173. $this->assertSame(
  174. 'missingWidgetThing',
  175. $ExceptionRenderer->__debugInfo()['method']
  176. );
  177. $this->assertSame(
  178. 'widget thing is missing',
  179. (string)$result->getBody(),
  180. 'Method declared in subclass converted to error400'
  181. );
  182. }
  183. /**
  184. * test that ExceptionRenderer subclasses properly convert framework errors.
  185. */
  186. public function testSubclassConvertingFrameworkErrors(): void
  187. {
  188. Configure::write('debug', false);
  189. $exception = new MissingControllerException('PostsController');
  190. $ExceptionRenderer = new MyCustomExceptionRenderer($exception);
  191. $result = $ExceptionRenderer->render();
  192. $this->assertMatchesRegularExpression(
  193. '/Not Found/',
  194. (string)$result->getBody(),
  195. 'Method declared in error handler not converted to error400. %s'
  196. );
  197. }
  198. /**
  199. * test things in the constructor.
  200. */
  201. public function testConstruction(): void
  202. {
  203. $exception = new NotFoundException('Page not found');
  204. $ExceptionRenderer = new ExceptionRenderer($exception);
  205. $this->assertInstanceOf(
  206. 'Cake\Controller\ErrorController',
  207. $ExceptionRenderer->__debugInfo()['controller']
  208. );
  209. $this->assertEquals($exception, $ExceptionRenderer->__debugInfo()['error']);
  210. }
  211. /**
  212. * test that exception message gets coerced when debug = 0
  213. */
  214. public function testExceptionMessageCoercion(): void
  215. {
  216. Configure::write('debug', false);
  217. $exception = new MissingActionException('Secret info not to be leaked');
  218. $ExceptionRenderer = new ExceptionRenderer($exception);
  219. $this->assertInstanceOf(
  220. 'Cake\Controller\ErrorController',
  221. $ExceptionRenderer->__debugInfo()['controller']
  222. );
  223. $this->assertEquals($exception, $ExceptionRenderer->__debugInfo()['error']);
  224. $result = (string)$ExceptionRenderer->render()->getBody();
  225. $this->assertSame('error400', $ExceptionRenderer->__debugInfo()['template']);
  226. $this->assertStringContainsString('Not Found', $result);
  227. $this->assertStringNotContainsString('Secret info not to be leaked', $result);
  228. }
  229. /**
  230. * test that helpers in custom CakeErrorController are not lost
  231. */
  232. public function testCakeErrorHelpersNotLost(): void
  233. {
  234. static::setAppNamespace();
  235. $exception = new NotFoundException();
  236. $renderer = new TestAppsExceptionRenderer($exception);
  237. $result = $renderer->render();
  238. $this->assertStringContainsString('<b>peeled</b>', (string)$result->getBody());
  239. }
  240. /**
  241. * test that unknown exception types with valid status codes are treated correctly.
  242. */
  243. public function testUnknownExceptionTypeWithExceptionThatHasA400Code(): void
  244. {
  245. $exception = new MissingWidgetThingException('coding fail.');
  246. $ExceptionRenderer = new ExceptionRenderer($exception);
  247. $response = $ExceptionRenderer->render();
  248. $this->assertSame(404, $response->getStatusCode());
  249. $this->assertFalse(method_exists($ExceptionRenderer, 'missingWidgetThing'), 'no method should exist.');
  250. $this->assertStringContainsString('coding fail', (string)$response->getBody(), 'Text should show up.');
  251. }
  252. /**
  253. * test that unknown exception types with valid status codes are treated correctly.
  254. */
  255. public function testUnknownExceptionTypeWithNoCodeIsA500(): void
  256. {
  257. $exception = new OutOfBoundsException('foul ball.');
  258. $ExceptionRenderer = new ExceptionRenderer($exception);
  259. $result = $ExceptionRenderer->render();
  260. $this->assertSame(500, $result->getStatusCode());
  261. $this->assertStringContainsString('foul ball.', (string)$result->getBody(), 'Text should show up as its debug mode.');
  262. }
  263. /**
  264. * test that unknown exceptions have messages ignored.
  265. */
  266. public function testUnknownExceptionInProduction(): void
  267. {
  268. Configure::write('debug', false);
  269. $exception = new OutOfBoundsException('foul ball.');
  270. $ExceptionRenderer = new ExceptionRenderer($exception);
  271. $response = $ExceptionRenderer->render();
  272. $result = (string)$response->getBody();
  273. $this->assertSame(500, $response->getStatusCode());
  274. $this->assertStringNotContainsString('foul ball.', $result, 'Text should no show up.');
  275. $this->assertStringContainsString('Internal Error', $result, 'Generic message only.');
  276. }
  277. /**
  278. * test that unknown exception types with valid status codes are treated correctly.
  279. */
  280. public function testUnknownExceptionTypeWithCodeHigherThan500(): void
  281. {
  282. $exception = new HttpException('foul ball.', 501);
  283. $ExceptionRenderer = new ExceptionRenderer($exception);
  284. $response = $ExceptionRenderer->render();
  285. $result = (string)$response->getBody();
  286. $this->assertSame(501, $response->getStatusCode());
  287. $this->assertStringContainsString('foul ball.', $result, 'Text should show up as its debug mode.');
  288. }
  289. /**
  290. * testerror400 method
  291. */
  292. public function testError400(): void
  293. {
  294. Router::reload();
  295. $request = new ServerRequest(['url' => 'posts/view/1000']);
  296. Router::setRequest($request);
  297. $exception = new NotFoundException('Custom message');
  298. $ExceptionRenderer = new ExceptionRenderer($exception);
  299. $response = $ExceptionRenderer->render();
  300. $result = (string)$response->getBody();
  301. $this->assertSame(404, $response->getStatusCode());
  302. $this->assertStringContainsString('<h2>Custom message</h2>', $result);
  303. $this->assertMatchesRegularExpression("/<strong>'.*?\/posts\/view\/1000'<\/strong>/", $result);
  304. }
  305. /**
  306. * testerror400 method when returning as JSON
  307. */
  308. public function testError400AsJson(): void
  309. {
  310. Router::reload();
  311. $request = new ServerRequest(['url' => 'posts/view/1000?sort=title&direction=desc']);
  312. $request = $request->withHeader('Accept', 'application/json');
  313. $request = $request->withHeader('Content-Type', 'application/json');
  314. Router::setRequest($request);
  315. $exception = new NotFoundException('Custom message');
  316. $exceptionLine = __LINE__ - 1;
  317. $ExceptionRenderer = new ExceptionRenderer($exception);
  318. $response = $ExceptionRenderer->render();
  319. $result = (string)$response->getBody();
  320. $expected = [
  321. 'message' => 'Custom message',
  322. 'url' => '/posts/view/1000?sort=title&amp;direction=desc',
  323. 'code' => 404,
  324. 'file' => __FILE__,
  325. 'line' => $exceptionLine,
  326. ];
  327. $this->assertEquals($expected, json_decode($result, true));
  328. $this->assertSame(404, $response->getStatusCode());
  329. }
  330. /**
  331. * test that error400 only modifies the messages on Cake Exceptions.
  332. */
  333. public function testerror400OnlyChangingCakeException(): void
  334. {
  335. Configure::write('debug', false);
  336. $exception = new NotFoundException('Custom message');
  337. $ExceptionRenderer = new ExceptionRenderer($exception);
  338. $result = $ExceptionRenderer->render();
  339. $this->assertStringContainsString('Custom message', (string)$result->getBody());
  340. $exception = new MissingActionException(['controller' => 'PostsController', 'action' => 'index']);
  341. $ExceptionRenderer = new ExceptionRenderer($exception);
  342. $result = $ExceptionRenderer->render();
  343. $this->assertStringContainsString('Not Found', (string)$result->getBody());
  344. }
  345. /**
  346. * test that error400 doesn't expose XSS
  347. */
  348. public function testError400NoInjection(): void
  349. {
  350. Router::reload();
  351. $request = new ServerRequest(['url' => 'pages/<span id=333>pink</span></id><script>document.body.style.background = t=document.getElementById(333).innerHTML;window.alert(t);</script>']);
  352. Router::setRequest($request);
  353. $exception = new NotFoundException('Custom message');
  354. $ExceptionRenderer = new ExceptionRenderer($exception);
  355. $result = (string)$ExceptionRenderer->render()->getBody();
  356. $this->assertStringNotContainsString('<script>document', $result);
  357. $this->assertStringNotContainsString('alert(t);</script>', $result);
  358. }
  359. /**
  360. * testError500 method
  361. */
  362. public function testError500Message(): void
  363. {
  364. $exception = new InternalErrorException('An Internal Error Has Occurred.');
  365. $ExceptionRenderer = new ExceptionRenderer($exception);
  366. $response = $ExceptionRenderer->render();
  367. $result = (string)$response->getBody();
  368. $this->assertSame(500, $response->getStatusCode());
  369. $this->assertStringContainsString('<h2>An Internal Error Has Occurred.</h2>', $result);
  370. $this->assertStringContainsString('An Internal Error Has Occurred.</p>', $result);
  371. }
  372. /**
  373. * testExceptionResponseHeader method
  374. */
  375. public function testExceptionResponseHeader(): void
  376. {
  377. $exception = new MethodNotAllowedException('Only allowing POST and DELETE');
  378. $exception->setHeader('Allow', ['POST', 'DELETE']);
  379. $ExceptionRenderer = new ExceptionRenderer($exception);
  380. $result = $ExceptionRenderer->render();
  381. $this->assertTrue($result->hasHeader('Allow'));
  382. $this->assertSame('POST,DELETE', $result->getHeaderLine('Allow'));
  383. $exception->setHeaders(['Allow' => 'GET']);
  384. $result = $ExceptionRenderer->render();
  385. $this->assertTrue($result->hasHeader('Allow'));
  386. $this->assertSame('GET', $result->getHeaderLine('Allow'));
  387. }
  388. /**
  389. * Tests setting exception response headers through core Exception
  390. */
  391. public function testExceptionDeprecatedResponseHeader(): void
  392. {
  393. $this->deprecated(function (): void {
  394. $exception = new CakeException('Should Not Set Headers');
  395. $exception->responseHeader(['Allow' => 'POST, DELETE']);
  396. $ExceptionRenderer = new ExceptionRenderer($exception);
  397. $result = $ExceptionRenderer->render();
  398. $this->assertTrue($result->hasHeader('Allow'));
  399. $this->assertSame('POST, DELETE', $result->getHeaderLine('Allow'));
  400. });
  401. }
  402. /**
  403. * testMissingController method
  404. */
  405. public function testMissingController(): void
  406. {
  407. $exception = new MissingControllerException([
  408. 'controller' => 'Posts',
  409. 'prefix' => '',
  410. 'plugin' => '',
  411. ]);
  412. $ExceptionRenderer = new MyCustomExceptionRenderer($exception);
  413. $result = (string)$ExceptionRenderer->render()->getBody();
  414. $this->assertSame(
  415. 'missingController',
  416. $ExceptionRenderer->__debugInfo()['template']
  417. );
  418. $this->assertStringContainsString('Missing Controller', $result);
  419. $this->assertStringContainsString('<em>PostsController</em>', $result);
  420. }
  421. /**
  422. * test missingController method
  423. */
  424. public function testMissingControllerLowerCase(): void
  425. {
  426. $exception = new MissingControllerException([
  427. 'controller' => 'posts',
  428. 'prefix' => '',
  429. 'plugin' => '',
  430. ]);
  431. $ExceptionRenderer = new MyCustomExceptionRenderer($exception);
  432. $result = (string)$ExceptionRenderer->render()->getBody();
  433. $this->assertSame(
  434. 'missingController',
  435. $ExceptionRenderer->__debugInfo()['template']
  436. );
  437. $this->assertStringContainsString('Missing Controller', $result);
  438. $this->assertStringContainsString('<em>PostsController</em>', $result);
  439. }
  440. /**
  441. * Returns an array of tests to run for the various Cake Exception classes.
  442. *
  443. * @return array
  444. */
  445. public static function exceptionProvider(): array
  446. {
  447. return [
  448. [
  449. new MissingActionException([
  450. 'controller' => 'PostsController',
  451. 'action' => 'index',
  452. 'prefix' => '',
  453. 'plugin' => '',
  454. ]),
  455. [
  456. '/Missing Method in PostsController/',
  457. '/<em>PostsController::index\(\)<\/em>/',
  458. ],
  459. 404,
  460. ],
  461. [
  462. new InvalidParameterException([
  463. 'template' => 'failed_coercion',
  464. 'passed' => 'test',
  465. 'type' => 'float',
  466. 'parameter' => 'age',
  467. 'controller' => 'TestController',
  468. 'action' => 'checkAge',
  469. 'prefix' => null,
  470. 'plugin' => null,
  471. ]),
  472. ['/The passed parameter or parameter type is invalid in <em>TestController::checkAge\(\)/'],
  473. 404,
  474. ],
  475. [
  476. new MissingActionException([
  477. 'controller' => 'PostsController',
  478. 'action' => 'index',
  479. 'prefix' => '',
  480. 'plugin' => '',
  481. ]),
  482. [
  483. '/Missing Method in PostsController/',
  484. '/<em>PostsController::index\(\)<\/em>/',
  485. ],
  486. 404,
  487. ],
  488. [
  489. new MissingTemplateException(['file' => '/posts/about.ctp']),
  490. [
  491. "/posts\/about.ctp/",
  492. ],
  493. 500,
  494. ],
  495. [
  496. new MissingLayoutException(['file' => 'layouts/my_layout.ctp']),
  497. [
  498. '/Missing Layout/',
  499. "/layouts\/my_layout.ctp/",
  500. ],
  501. 500,
  502. ],
  503. [
  504. new MissingHelperException(['class' => 'MyCustomHelper']),
  505. [
  506. '/Missing Helper/',
  507. '/<em>MyCustomHelper<\/em> could not be found./',
  508. '/Create the class <em>MyCustomHelper<\/em> below in file:/',
  509. '/(\/|\\\)MyCustomHelper.php/',
  510. ],
  511. 500,
  512. ],
  513. [
  514. new MissingBehaviorException(['class' => 'MyCustomBehavior']),
  515. [
  516. '/Missing Behavior/',
  517. '/Create the class <em>MyCustomBehavior<\/em> below in file:/',
  518. '/(\/|\\\)MyCustomBehavior.php/',
  519. ],
  520. 500,
  521. ],
  522. [
  523. new MissingComponentException(['class' => 'SideboxComponent']),
  524. [
  525. '/Missing Component/',
  526. '/Create the class <em>SideboxComponent<\/em> below in file:/',
  527. '/(\/|\\\)SideboxComponent.php/',
  528. ],
  529. 500,
  530. ],
  531. [
  532. new MissingDatasourceConfigException(['name' => 'MyDatasourceConfig']),
  533. [
  534. '/Missing Datasource Configuration/',
  535. '/<em>MyDatasourceConfig<\/em> was not found/',
  536. ],
  537. 500,
  538. ],
  539. [
  540. new MissingDatasourceException(['class' => 'MyDatasource', 'plugin' => 'MyPlugin']),
  541. [
  542. '/Missing Datasource/',
  543. '/<em>MyPlugin.MyDatasource<\/em> could not be found./',
  544. ],
  545. 500,
  546. ],
  547. [
  548. new MissingMailerActionException([
  549. 'mailer' => 'UserMailer',
  550. 'action' => 'welcome',
  551. 'prefix' => '',
  552. 'plugin' => '',
  553. ]),
  554. [
  555. '/Missing Method in UserMailer/',
  556. '/<em>UserMailer::welcome\(\)<\/em>/',
  557. ],
  558. 500,
  559. ],
  560. [
  561. new Exception('boom'),
  562. [
  563. '/Internal Error/',
  564. ],
  565. 500,
  566. ],
  567. [
  568. new RuntimeException('another boom'),
  569. [
  570. '/Internal Error/',
  571. ],
  572. 500,
  573. ],
  574. [
  575. new CakeException('base class'),
  576. ['/Internal Error/'],
  577. 500,
  578. ],
  579. [
  580. new HttpException('Network Authentication Required', 511),
  581. ['/Network Authentication Required/'],
  582. 511,
  583. ],
  584. ];
  585. }
  586. /**
  587. * Test the various Cake Exception sub classes
  588. *
  589. * @dataProvider exceptionProvider
  590. */
  591. public function testCakeExceptionHandling(Exception $exception, array $patterns, int $code): void
  592. {
  593. $exceptionRenderer = new ExceptionRenderer($exception);
  594. $response = $exceptionRenderer->render();
  595. $this->assertEquals($code, $response->getStatusCode());
  596. $body = (string)$response->getBody();
  597. foreach ($patterns as $pattern) {
  598. $this->assertMatchesRegularExpression($pattern, $body);
  599. }
  600. }
  601. /**
  602. * Test that class names not ending in Exception are not mangled.
  603. */
  604. public function testExceptionNameMangling(): void
  605. {
  606. $exceptionRenderer = new MyCustomExceptionRenderer(new MissingWidgetThing());
  607. $result = (string)$exceptionRenderer->render()->getBody();
  608. $this->assertStringContainsString('widget thing is missing', $result);
  609. // Custom method should be called even when debug is off.
  610. Configure::write('debug', false);
  611. $exceptionRenderer = new MyCustomExceptionRenderer(new MissingWidgetThing());
  612. $result = (string)$exceptionRenderer->render()->getBody();
  613. $this->assertStringContainsString('widget thing is missing', $result);
  614. }
  615. /**
  616. * Test exceptions being raised when helpers are missing.
  617. */
  618. public function testMissingRenderSafe(): void
  619. {
  620. $exception = new MissingHelperException(['class' => 'Fail']);
  621. $ExceptionRenderer = new MyCustomExceptionRenderer($exception);
  622. /** @var \Cake\Controller\Controller|\PHPUnit\Framework\MockObject\MockObject $controller */
  623. $controller = $this->getMockBuilder('Cake\Controller\Controller')
  624. ->onlyMethods(['render'])
  625. ->getMock();
  626. $controller->viewBuilder()->setHelpers(['Fail', 'Boom'], false);
  627. $controller->setRequest(new ServerRequest());
  628. $controller->expects($this->once())
  629. ->method('render')
  630. ->with('missingHelper')
  631. ->will($this->throwException($exception));
  632. $ExceptionRenderer->setController($controller);
  633. $response = $ExceptionRenderer->render();
  634. $helpers = $controller->viewBuilder()->getHelpers();
  635. sort($helpers);
  636. $this->assertEquals([], $helpers);
  637. $this->assertStringContainsString('Helper class Fail', (string)$response->getBody());
  638. }
  639. /**
  640. * Test that exceptions in beforeRender() are handled by outputMessageSafe
  641. */
  642. public function testRenderExceptionInBeforeRender(): void
  643. {
  644. $exception = new NotFoundException('Not there, sorry');
  645. $ExceptionRenderer = new MyCustomExceptionRenderer($exception);
  646. /** @var \Cake\Controller\Controller|\PHPUnit\Framework\MockObject\MockObject $controller */
  647. $controller = $this->getMockBuilder('Cake\Controller\Controller')
  648. ->onlyMethods(['beforeRender'])
  649. ->getMock();
  650. $controller->request = new ServerRequest();
  651. $controller->expects($this->any())
  652. ->method('beforeRender')
  653. ->will($this->throwException($exception));
  654. $ExceptionRenderer->setController($controller);
  655. $response = $ExceptionRenderer->render();
  656. $this->assertStringContainsString('Not there, sorry', (string)$response->getBody());
  657. }
  658. /**
  659. * Test that missing layoutPath don't cause other fatal errors.
  660. */
  661. public function testMissingLayoutPathRenderSafe(): void
  662. {
  663. $this->called = false;
  664. $exception = new NotFoundException();
  665. $ExceptionRenderer = new MyCustomExceptionRenderer($exception);
  666. $controller = new Controller();
  667. $controller->viewBuilder()->setHelpers(['Fail', 'Boom'], false);
  668. $controller->getEventManager()->on(
  669. 'Controller.beforeRender',
  670. function (EventInterface $event): void {
  671. $this->called = true;
  672. $event->getSubject()->viewBuilder()->setLayoutPath('boom');
  673. }
  674. );
  675. $controller->setRequest(new ServerRequest());
  676. $ExceptionRenderer->setController($controller);
  677. $response = $ExceptionRenderer->render();
  678. $this->assertSame('text/html', $response->getType());
  679. $this->assertStringContainsString('Not Found', (string)$response->getBody());
  680. $this->assertTrue($this->called, 'Listener added was not triggered.');
  681. $this->assertSame('', $controller->viewBuilder()->getLayoutPath());
  682. $this->assertSame('Error', $controller->viewBuilder()->getTemplatePath());
  683. }
  684. /**
  685. * Test that missing layout don't cause other fatal errors.
  686. */
  687. public function testMissingLayoutRenderSafe(): void
  688. {
  689. $this->called = false;
  690. $exception = new NotFoundException();
  691. $ExceptionRenderer = new MyCustomExceptionRenderer($exception);
  692. $controller = new Controller();
  693. $controller->getEventManager()->on(
  694. 'Controller.beforeRender',
  695. function (EventInterface $event): void {
  696. $this->called = true;
  697. $event->getSubject()->viewBuilder()->setTemplatePath('Error');
  698. $event->getSubject()->viewBuilder()->setLayout('does-not-exist');
  699. }
  700. );
  701. $controller->setRequest(new ServerRequest());
  702. $ExceptionRenderer->setController($controller);
  703. $response = $ExceptionRenderer->render();
  704. $this->assertSame('text/html', $response->getType());
  705. $this->assertStringContainsString('Not Found', (string)$response->getBody());
  706. $this->assertTrue($this->called, 'Listener added was not triggered.');
  707. $this->assertSame('', $controller->viewBuilder()->getLayoutPath());
  708. $this->assertSame('Error', $controller->viewBuilder()->getTemplatePath());
  709. }
  710. /**
  711. * Test that missing plugin disables Controller::$plugin if the two are the same plugin.
  712. */
  713. public function testMissingPluginRenderSafe(): void
  714. {
  715. $exception = new NotFoundException();
  716. $ExceptionRenderer = new MyCustomExceptionRenderer($exception);
  717. /** @var \Cake\Controller\Controller|\PHPUnit\Framework\MockObject\MockObject $controller */
  718. $controller = $this->getMockBuilder('Cake\Controller\Controller')
  719. ->onlyMethods(['render'])
  720. ->getMock();
  721. $controller->setPlugin('TestPlugin');
  722. $controller->request = new ServerRequest();
  723. $exception = new MissingPluginException(['plugin' => 'TestPlugin']);
  724. $controller->expects($this->once())
  725. ->method('render')
  726. ->with('error400')
  727. ->will($this->throwException($exception));
  728. $ExceptionRenderer->setController($controller);
  729. $response = $ExceptionRenderer->render();
  730. $body = (string)$response->getBody();
  731. $this->assertStringNotContainsString('test plugin error500', $body);
  732. $this->assertStringContainsString('Not Found', $body);
  733. }
  734. /**
  735. * Test that missing plugin doesn't disable Controller::$plugin if the two aren't the same plugin.
  736. */
  737. public function testMissingPluginRenderSafeWithPlugin(): void
  738. {
  739. $this->loadPlugins(['TestPlugin']);
  740. $exception = new NotFoundException();
  741. $ExceptionRenderer = new MyCustomExceptionRenderer($exception);
  742. /** @var \Cake\Controller\Controller|\PHPUnit\Framework\MockObject\MockObject $controller */
  743. $controller = $this->getMockBuilder('Cake\Controller\Controller')
  744. ->onlyMethods(['render'])
  745. ->getMock();
  746. $controller->setPlugin('TestPlugin');
  747. $controller->request = new ServerRequest();
  748. $exception = new MissingPluginException(['plugin' => 'TestPluginTwo']);
  749. $controller->expects($this->once())
  750. ->method('render')
  751. ->with('error400')
  752. ->will($this->throwException($exception));
  753. $ExceptionRenderer->setController($controller);
  754. $response = $ExceptionRenderer->render();
  755. $body = (string)$response->getBody();
  756. $this->assertStringContainsString('test plugin error500', $body);
  757. $this->assertStringContainsString('Not Found', $body);
  758. }
  759. /**
  760. * Test that exceptions can be rendered when a request hasn't been registered
  761. * with Router
  762. */
  763. public function testRenderWithNoRequest(): void
  764. {
  765. Router::reload();
  766. $this->assertNull(Router::getRequest());
  767. $exception = new Exception('Terrible');
  768. $ExceptionRenderer = new ExceptionRenderer($exception);
  769. $result = $ExceptionRenderer->render();
  770. $this->assertStringContainsString('Internal Error', (string)$result->getBody());
  771. $this->assertSame(500, $result->getStatusCode());
  772. }
  773. /**
  774. * Test that router request parameters are applied when the passed
  775. * request has no params.
  776. */
  777. public function testRenderInheritRoutingParams(): void
  778. {
  779. $routerRequest = new ServerRequest([
  780. 'params' => [
  781. 'controller' => 'Articles',
  782. 'action' => 'index',
  783. 'plugin' => null,
  784. 'pass' => [],
  785. '_ext' => 'json',
  786. ],
  787. ]);
  788. // Simulate a request having routing applied and stored in router
  789. Router::setRequest($routerRequest);
  790. $exceptionRenderer = new ExceptionRenderer(new Exception('Terrible'), new ServerRequest());
  791. $exceptionRenderer->render();
  792. $properties = $exceptionRenderer->__debugInfo();
  793. /** @var \Cake\Http\ServerRequest $request */
  794. $request = $properties['controller']->getRequest();
  795. foreach (['controller', 'action', '_ext'] as $key) {
  796. $this->assertSame($routerRequest->getParam($key), $request->getParam($key));
  797. }
  798. }
  799. /**
  800. * Test that rendering exceptions triggers shutdown events.
  801. */
  802. public function testRenderShutdownEvents(): void
  803. {
  804. $fired = [];
  805. $listener = function (EventInterface $event) use (&$fired): void {
  806. $fired[] = $event->getName();
  807. };
  808. $events = EventManager::instance();
  809. $events->on('Controller.shutdown', $listener);
  810. $exception = new Exception('Terrible');
  811. $renderer = new ExceptionRenderer($exception);
  812. $renderer->render();
  813. $expected = ['Controller.shutdown'];
  814. $this->assertEquals($expected, $fired);
  815. }
  816. /**
  817. * test that subclass methods fire shutdown events.
  818. */
  819. public function testSubclassTriggerShutdownEvents(): void
  820. {
  821. $fired = [];
  822. $listener = function (EventInterface $event) use (&$fired): void {
  823. $fired[] = $event->getName();
  824. };
  825. $events = EventManager::instance();
  826. $events->on('Controller.shutdown', $listener);
  827. $exception = new MissingWidgetThingException('Widget not found');
  828. $renderer = new MyCustomExceptionRenderer($exception);
  829. $renderer->render();
  830. $expected = ['Controller.shutdown'];
  831. $this->assertEquals($expected, $fired);
  832. }
  833. /**
  834. * Tests the output of rendering a PDOException
  835. */
  836. public function testPDOException(): void
  837. {
  838. $exception = new MyPDOException('There was an error in the SQL query');
  839. $exception->queryString = 'SELECT * from poo_query < 5 and :seven';
  840. $exception->params = ['seven' => 7];
  841. $ExceptionRenderer = new ExceptionRenderer($exception);
  842. $response = $ExceptionRenderer->render();
  843. $this->assertSame(500, $response->getStatusCode());
  844. $result = (string)$response->getBody();
  845. $this->assertStringContainsString('Database Error', $result);
  846. $this->assertStringContainsString('There was an error in the SQL query', $result);
  847. $this->assertStringContainsString(h('SELECT * from poo_query < 5 and :seven'), $result);
  848. $this->assertStringContainsString("'seven' => (int) 7", $result);
  849. }
  850. }