ExceptionRendererTest.php 32 KB

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