From e2c8e20afa589aa85846564236f2a1c46d480eac Mon Sep 17 00:00:00 2001 From: mark_story Date: Sun, 14 Nov 2010 22:20:29 -0500 Subject: [PATCH] Pulling exception page rendering out into a separate class, ErrorHandler felt very large and confusing, as it had a few too many jobs. --- cake/libs/error_handler.php | 185 +----- cake/libs/exception_renderer.php | 233 +++++++ cake/tests/cases/libs/error_handler.test.php | 382 ------------ .../cases/libs/exception_renderer.test.php | 573 ++++++++++++++++++ 4 files changed, 809 insertions(+), 564 deletions(-) create mode 100644 cake/libs/exception_renderer.php create mode 100644 cake/tests/cases/libs/exception_renderer.test.php diff --git a/cake/libs/error_handler.php b/cake/libs/error_handler.php index 73deafc9c..33146a289 100644 --- a/cake/libs/error_handler.php +++ b/cake/libs/error_handler.php @@ -54,119 +54,17 @@ */ class ErrorHandler { -/** - * Controller instance. - * - * @var Controller - * @access public - */ - public $controller = null; - -/** - * template to render for CakeException - * - * @var string - */ - public $template = ''; - -/** - * The method corresponding to the Exception this object is for. - * - * @var string - */ - public $method = ''; - -/** - * The exception being handled. - * - * @var Exception - */ - public $error = null; - -/** - * Creates the controller to perform rendering on the error response. - * If the error is a CakeException it will be converted to either a 400 or a 500 - * code error depending on the code used to construct the error. - * - * @param string $method Method producing the error - * @param array $messages Error messages - */ - function __construct(Exception $exception) { - App::import('Core', 'Sanitize'); - - $this->controller = $this->_getController($exception); - - if (method_exists($this->controller, 'apperror')) { - return $this->controller->appError($exception); - } - $method = $template = Inflector::variable(str_replace('Exception', '', get_class($exception))); - $code = $exception->getCode(); - - $methodExists = method_exists($this, $method); - - if ($exception instanceof CakeException && !$methodExists) { - $method = '_cakeError'; - if ($template == 'internalError') { - $template = 'error500'; - } - } elseif (!$methodExists) { - $method = 'error500'; - if ($code >= 400) { - $method = 'error400'; - } - } - - if (Configure::read('debug') == 0) { - $parentClass = get_parent_class($this); - if ($parentClass != 'ErrorHandler') { - $method = 'error400'; - } - $parentMethods = (array)get_class_methods($parentClass); - if (in_array($method, $parentMethods)) { - $method = 'error400'; - } - if ($code == 500) { - $method = 'error500'; - } - } - $this->template = $template; - $this->method = $method; - $this->error = $exception; - } - -/** - * Get the controller instance to handle the exception. - * Override this method in subclasses to customize the controller used. - * This method returns the built in `CakeErrorController` normally, or if an error is repeated - * a bare controller will be used. - * - * @param Exception $exception The exception to get a controller for. - * @return Controller - */ - protected function _getController($exception) { - static $__previousError = null; - App::import('Controller', 'CakeError'); - - if ($__previousError != $exception) { - $__previousError = $exception; - $controller = new CakeErrorController(); - } else { - $controller = new Controller(); - $controller->viewPath = 'errors'; - } - return $controller; - } - /** * Set as the default exception handler by the CakePHP bootstrap process. * * This will either use an AppError class if your application has one, - * or use the default ErrorHandler. + * or use the default ExceptionRenderer. * * @return void * @see http://php.net/manual/en/function.set-exception-handler.php */ public static function handleException(Exception $exception) { + App::import('Core', 'ExceptionRenderer'); if (file_exists(APP . 'app_error.php') || class_exists('AppError')) { if (!class_exists('AppError')) { require(APP . 'app_error.php'); @@ -174,7 +72,7 @@ class ErrorHandler { $AppError = new AppError($exception); return $AppError->render(); } - $error = new ErrorHandler($exception); + $error = new ExceptionRenderer($exception); $error->render(); } @@ -271,81 +169,4 @@ class ErrorHandler { } return array($error, $log); } - -/** - * Renders the response for the exception. - * - * @return void - */ - public function render() { - call_user_func_array(array($this, $this->method), array($this->error)); - } - -/** - * Generic handler for the internal framework errors CakePHP can generate. - * - * @param CakeExeption $error - * @return void - */ - protected function _cakeError(CakeException $error) { - $url = Router::normalize($this->controller->request->here); - $code = $error->getCode(); - $this->controller->response->statusCode($code); - $this->controller->set(array( - 'code' => $code, - 'url' => h($url), - 'name' => $error->getMessage(), - 'error' => $error, - )); - $this->controller->set($error->getAttributes()); - $this->_outputMessage($this->template); - } - -/** - * Convenience method to display a 400 series page. - * - * @param array $params Parameters for controller - */ - public function error400($error) { - $message = $error->getMessage(); - if (Configure::read('debug') == 0 && $error instanceof CakeException) { - $message = __('Not Found'); - } - $url = Router::normalize($this->controller->request->here); - $this->controller->response->statusCode($error->getCode()); - $this->controller->set(array( - 'name' => $message, - 'url' => h($url), - 'error' => $error, - )); - $this->_outputMessage('error400'); - } - -/** - * Convenience method to display a 500 page. - * - * @param array $params Parameters for controller - */ - public function error500($error) { - $url = Router::normalize($this->controller->request->here); - $code = ($error->getCode() > 500) ? $error->getCode() : 500; - $this->controller->response->statusCode($code); - $this->controller->set(array( - 'name' => __('An Internal Error Has Occurred'), - 'message' => h($url), - 'error' => $error, - )); - $this->_outputMessage('error500'); - } - -/** - * Generate the response using the controller object. - * - * @param string $template The template to render. - */ - protected function _outputMessage($template) { - $this->controller->render($template); - $this->controller->afterFilter(); - $this->controller->response->send(); - } } diff --git a/cake/libs/exception_renderer.php b/cake/libs/exception_renderer.php new file mode 100644 index 000000000..cb795d212 --- /dev/null +++ b/cake/libs/exception_renderer.php @@ -0,0 +1,233 @@ + 1. + * When debug < 1 a CakeException will render 404 or 500 errors. If an uncaught exception is thrown + * and it is a type that ExceptionHandler does not know about it will be treated as a 500 error. + * + * ### Implementing application specific exception handling + * + * You can implement application specific exception handling in one of a few ways: + * + * - Create a AppController::appError(); + * - Create an AppError class. + * + * #### Using AppController::appError(); + * + * This controller method is called instead of the default exception handling. It receives the + * thrown exception as its only argument. You should implement your error handling in that method. + * + * #### Using an AppError class + * + * This approach gives more flexibility and power in how you handle exceptions. You can create + * `app/libs/app_error.php` and create a class called `AppError`. The core ErrorHandler class + * will attempt to construct this class and let it handle the exception. This provides a more + * flexible way to handle exceptions in your application. + * + * @package cake + * @subpackage cake.cake.libs + */ +class ExceptionRenderer { + +/** + * Controller instance. + * + * @var Controller + * @access public + */ + public $controller = null; + +/** + * template to render for CakeException + * + * @var string + */ + public $template = ''; + +/** + * The method corresponding to the Exception this object is for. + * + * @var string + */ + public $method = ''; + +/** + * The exception being handled. + * + * @var Exception + */ + public $error = null; + +/** + * Creates the controller to perform rendering on the error response. + * If the error is a CakeException it will be converted to either a 400 or a 500 + * code error depending on the code used to construct the error. + * + * @param string $method Method producing the error + * @param array $messages Error messages + */ + function __construct(Exception $exception) { + App::import('Core', 'Sanitize'); + + $this->controller = $this->_getController($exception); + + if (method_exists($this->controller, 'apperror')) { + return $this->controller->appError($exception); + } + $method = $template = Inflector::variable(str_replace('Exception', '', get_class($exception))); + $code = $exception->getCode(); + + $methodExists = method_exists($this, $method); + + if ($exception instanceof CakeException && !$methodExists) { + $method = '_cakeError'; + if ($template == 'internalError') { + $template = 'error500'; + } + } elseif (!$methodExists) { + $method = 'error500'; + if ($code >= 400) { + $method = 'error400'; + } + } + + if (Configure::read('debug') == 0) { + $parentClass = get_parent_class($this); + if ($parentClass != __CLASS__) { + $method = 'error400'; + } + $parentMethods = (array)get_class_methods($parentClass); + if (in_array($method, $parentMethods)) { + $method = 'error400'; + } + if ($code == 500) { + $method = 'error500'; + } + } + $this->template = $template; + $this->method = $method; + $this->error = $exception; + } + +/** + * Get the controller instance to handle the exception. + * Override this method in subclasses to customize the controller used. + * This method returns the built in `CakeErrorController` normally, or if an error is repeated + * a bare controller will be used. + * + * @param Exception $exception The exception to get a controller for. + * @return Controller + */ + protected function _getController($exception) { + static $__previousError = null; + App::import('Controller', 'CakeError'); + + if ($__previousError != $exception) { + $__previousError = $exception; + $controller = new CakeErrorController(); + } else { + $controller = new Controller(); + $controller->viewPath = 'errors'; + } + return $controller; + } + +/** + * Renders the response for the exception. + * + * @return void + */ + public function render() { + call_user_func_array(array($this, $this->method), array($this->error)); + } + +/** + * Generic handler for the internal framework errors CakePHP can generate. + * + * @param CakeExeption $error + * @return void + */ + protected function _cakeError(CakeException $error) { + $url = Router::normalize($this->controller->request->here); + $code = $error->getCode(); + $this->controller->response->statusCode($code); + $this->controller->set(array( + 'code' => $code, + 'url' => h($url), + 'name' => $error->getMessage(), + 'error' => $error, + )); + $this->controller->set($error->getAttributes()); + $this->_outputMessage($this->template); + } + +/** + * Convenience method to display a 400 series page. + * + * @param array $params Parameters for controller + */ + public function error400($error) { + $message = $error->getMessage(); + if (Configure::read('debug') == 0 && $error instanceof CakeException) { + $message = __('Not Found'); + } + $url = Router::normalize($this->controller->request->here); + $this->controller->response->statusCode($error->getCode()); + $this->controller->set(array( + 'name' => $message, + 'url' => h($url), + 'error' => $error, + )); + $this->_outputMessage('error400'); + } + +/** + * Convenience method to display a 500 page. + * + * @param array $params Parameters for controller + */ + public function error500($error) { + $url = Router::normalize($this->controller->request->here); + $code = ($error->getCode() > 500) ? $error->getCode() : 500; + $this->controller->response->statusCode($code); + $this->controller->set(array( + 'name' => __('An Internal Error Has Occurred'), + 'message' => h($url), + 'error' => $error, + )); + $this->_outputMessage('error500'); + } + +/** + * Generate the response using the controller object. + * + * @param string $template The template to render. + */ + protected function _outputMessage($template) { + $this->controller->render($template); + $this->controller->afterFilter(); + $this->controller->response->send(); + } +} \ No newline at end of file diff --git a/cake/tests/cases/libs/error_handler.test.php b/cake/tests/cases/libs/error_handler.test.php index e09e4fc85..490ce1a87 100644 --- a/cake/tests/cases/libs/error_handler.test.php +++ b/cake/tests/cases/libs/error_handler.test.php @@ -188,16 +188,6 @@ class ErrorHandlerTest extends CakeTestCase { } } -/** - * Mocks out the response on the errorhandler object so headers aren't modified. - * - * @return void - */ - protected function _mockResponse($error) { - $error->controller->response = $this->getMock('CakeResponse', array('_sendHeader')); - return $error; - } - /** * test error handling when debug is on, an error should be printed from Debugger. * @@ -288,376 +278,4 @@ class ErrorHandlerTest extends CakeTestCase { $this->assertPattern('/Kaboom!/', $result, 'message missing.'); } -/** - * test that methods declared in an ErrorHandler subclass are not converted - * into error400 when debug > 0 - * - * @return void - */ - function testSubclassMethodsNotBeingConvertedToError() { - Configure::write('debug', 2); - - $exception = new MissingWidgetThingException('Widget not found'); - $ErrorHandler = $this->_mockResponse(new MyCustomErrorHandler($exception)); - - ob_start(); - $ErrorHandler->render(); - $result = ob_get_clean(); - - $this->assertEqual($result, 'widget thing is missing'); - } - -/** - * test that subclass methods are not converted when debug = 0 - * - * @return void - */ - function testSubclassMethodsNotBeingConvertedDebug0() { - Configure::write('debug', 0); - $exception = new MissingWidgetThingException('Widget not found'); - $ErrorHandler = $this->_mockResponse(new MyCustomErrorHandler($exception)); - - $this->assertEqual('missingWidgetThing', $ErrorHandler->method); - - ob_start(); - $ErrorHandler->render(); - $result = ob_get_clean(); - - $this->assertEqual($result, 'widget thing is missing', 'Method declared in subclass converted to error400'); - } - -/** - * test that ErrorHandler subclasses properly convert framework errors. - * - * @return void - */ - function testSubclassConvertingFrameworkErrors() { - Configure::write('debug', 0); - - $exception = new MissingControllerException('PostsController'); - $ErrorHandler = $this->_mockResponse(new MyCustomErrorHandler($exception)); - - $this->assertEqual('error400', $ErrorHandler->method); - - ob_start(); - $ErrorHandler->render(); - $result = ob_get_clean(); - - $this->assertPattern('/Not Found/', $result, 'Method declared in error handler not converted to error400. %s'); - } - -/** - * test things in the constructor. - * - * @return void - */ - function testConstruction() { - $exception = new NotFoundException('Page not found'); - $ErrorHandler = new ErrorHandler($exception); - - $this->assertType('CakeErrorController', $ErrorHandler->controller); - $this->assertEquals('error400', $ErrorHandler->method); - $this->assertEquals($exception, $ErrorHandler->error); - } - -/** - * test that method gets coerced when debug = 0 - * - * @return void - */ - function testErrorMethodCoercion() { - Configure::write('debug', 0); - $exception = new MissingActionException('Page not found'); - $ErrorHandler = new ErrorHandler($exception); - - $this->assertType('CakeErrorController', $ErrorHandler->controller); - $this->assertEquals('error400', $ErrorHandler->method); - $this->assertEquals($exception, $ErrorHandler->error); - } - -/** - * test that unknown exception types with valid status codes are treated correctly. - * - * @return void - */ - function testUnknownExceptionTypeWithExceptionThatHasA400Code() { - $exception = new MissingWidgetThingException('coding fail.'); - $ErrorHandler = new ErrorHandler($exception); - $ErrorHandler->controller->response = $this->getMock('CakeResponse', array('statusCode', '_sendHeader')); - $ErrorHandler->controller->response->expects($this->once())->method('statusCode')->with(404); - - ob_start(); - $ErrorHandler->render(); - $results = ob_get_clean(); - - $this->assertFalse(method_exists($ErrorHandler, 'missingWidgetThing'), 'no method should exist.'); - $this->assertEquals('error400', $ErrorHandler->method, 'incorrect method coercion.'); - } - -/** - * test that unknown exception types with valid status codes are treated correctly. - * - * @return void - */ - function testUnknownExceptionTypeWithNoCodeIsA500() { - $exception = new OutOfBoundsException('foul ball.'); - $ErrorHandler = new ErrorHandler($exception); - $ErrorHandler->controller->response = $this->getMock('CakeResponse', array('statusCode', '_sendHeader')); - $ErrorHandler->controller->response->expects($this->once())->method('statusCode')->with(500); - - ob_start(); - $ErrorHandler->render(); - $results = ob_get_clean(); - - $this->assertEquals('error500', $ErrorHandler->method, 'incorrect method coercion.'); - } - -/** - * testerror400 method - * - * @access public - * @return void - */ - function testError400() { - Router::reload(); - - $request = new CakeRequest('posts/view/1000', false); - Router::setRequestInfo($request); - - $exception = new NotFoundException('Custom message'); - $ErrorHandler = new ErrorHandler($exception); - $ErrorHandler->controller->response = $this->getMock('CakeResponse', array('statusCode', '_sendHeader')); - $ErrorHandler->controller->response->expects($this->once())->method('statusCode')->with(404); - - ob_start(); - $ErrorHandler->render(); - $result = ob_get_clean(); - - $this->assertPattern('/

Custom message<\/h2>/', $result); - $this->assertPattern("/'\/posts\/view\/1000'<\/strong>/", $result); - } - -/** - * test that error400 only modifies the messages on CakeExceptions. - * - * @return void - */ - function testerror400OnlyChangingCakeException() { - Configure::write('debug', 0); - - $exception = new NotFoundException('Custom message'); - $ErrorHandler = $this->_mockResponse(new ErrorHandler($exception)); - - ob_start(); - $ErrorHandler->render(); - $result = ob_get_clean(); - $this->assertContains('Custom message', $result); - - $exception = new MissingActionException(array('controller' => 'PostsController', 'action' => 'index')); - $ErrorHandler = $this->_mockResponse(new ErrorHandler($exception)); - - ob_start(); - $ErrorHandler->render(); - $result = ob_get_clean(); - $this->assertContains('Not Found', $result); - } -/** - * test that error400 doesn't expose XSS - * - * @return void - */ - function testError400NoInjection() { - Router::reload(); - - $request = new CakeRequest('pages/pink', false); - Router::setRequestInfo($request); - - $exception = new NotFoundException('Custom message'); - $ErrorHandler = $this->_mockResponse(new ErrorHandler($exception)); - - ob_start(); - $ErrorHandler->render(); - $result = ob_get_clean(); - - $this->assertNoPattern('##', $result); - } - -/** - * testError500 method - * - * @access public - * @return void - */ - function testError500Message() { - $exception = new InternalErrorException('An Internal Error Has Occurred'); - $ErrorHandler = new ErrorHandler($exception); - $ErrorHandler->controller->response = $this->getMock('CakeResponse', array('statusCode', '_sendHeader')); - $ErrorHandler->controller->response->expects($this->once())->method('statusCode')->with(500); - - ob_start(); - $ErrorHandler->render(); - $result = ob_get_clean(); - - $this->assertPattern('/

An Internal Error Has Occurred<\/h2>/', $result); - } - -/** - * testMissingController method - * - * @access public - * @return void - */ - function testMissingController() { - $exception = new MissingControllerException(array('controller' => 'PostsController')); - $ErrorHandler = $this->_mockResponse(new ErrorHandler($exception)); - - ob_start(); - $ErrorHandler->render(); - $result = ob_get_clean(); - - $this->assertPattern('/

Missing Controller<\/h2>/', $result); - $this->assertPattern('/PostsController<\/em>/', $result); - } - -/** - * Returns an array of tests to run for the various CakeException classes. - * - * @return void - */ - public static function testProvider() { - return array( - array( - new MissingActionException(array('controller' => 'PostsController', 'action' => 'index')), - array( - '/

Missing Method in PostsController<\/h2>/', - '/PostsController::<\/em>index\(\)<\/em>/' - ), - 404 - ), - array( - new PrivateActionException(array('controller' => 'PostsController' , 'action' => '_secretSauce')), - array( - '/

Private Method in PostsController<\/h2>/', - '/PostsController::<\/em>_secretSauce\(\)<\/em>/' - ), - 404 - ), - array( - new MissingTableException(array('table' => 'articles', 'class' => 'Article')), - array( - '/

Missing Database Table<\/h2>/', - '/table articles<\/em> for model Article<\/em>/' - ), - 500 - ), - array( - new MissingDatabaseException(array('connection' => 'default')), - array( - '/

Missing Database Connection<\/h2>/', - '/Confirm you have created the file/' - ), - 500 - ), - array( - new MissingViewException(array('file' => '/posts/about.ctp')), - array( - "/posts\/about.ctp/" - ), - 500 - ), - array( - new MissingLayoutException(array('file' => 'layouts/my_layout.ctp')), - array( - "/Missing Layout/", - "/layouts\/my_layout.ctp/" - ), - 500 - ), - array( - new MissingConnectionException(array('class' => 'Article')), - array( - '/

Missing Database Connection<\/h2>/', - '/Article requires a database connection/' - ), - 500 - ), - array( - new MissingHelperFileException(array('file' => 'my_custom.php', 'class' => 'MyCustomHelper')), - array( - '/

Missing Helper File<\/h2>/', - '/Create the class below in file:/', - '/(\/|\\\)my_custom.php/' - ), - 500 - ), - array( - new MissingHelperClassException(array('file' => 'my_custom.php', 'class' => 'MyCustomHelper')), - array( - '/

Missing Helper Class<\/h2>/', - '/The helper class MyCustomHelper<\/em> can not be found or does not exist./', - '/(\/|\\\)my_custom.php/', - ), - 500 - ), - array( - new MissingBehaviorFileException(array('file' => 'my_custom.php', 'class' => 'MyCustomBehavior')), - array( - '/

Missing Behavior File<\/h2>/', - '/Create the class below in file:/', - '/(\/|\\\)my_custom.php/', - ), - 500 - ), - array( - new MissingBehaviorClassException(array('file' => 'my_custom.php', 'class' => 'MyCustomBehavior')), - array( - '/The behavior class MyCustomBehavior<\/em> can not be found or does not exist./', - '/(\/|\\\)my_custom.php/' - ), - 500 - ), - array( - new MissingComponentFileException(array('file' => 'sidebox.php', 'class' => 'SideboxComponent')), - array( - '/

Missing Component File<\/h2>/', - '/Create the class SideboxComponent<\/em> in file:/', - '/(\/|\\\)sidebox.php/' - ), - 500 - ), - array( - new MissingComponentClassException(array('file' => 'sidebox.php', 'class' => 'SideboxComponent')), - array( - '/

Missing Component Class<\/h2>/', - '/Create the class SideboxComponent<\/em> in file:/', - '/(\/|\\\)sidebox.php/' - ), - 500 - ) - - ); - } - -/** - * Test the various CakeException sub classes - * - * @dataProvider testProvider - * @return void - */ - function testCakeExceptionHandling($exception, $patterns, $code) { - $ErrorHandler = new ErrorHandler($exception); - $ErrorHandler->controller->response = $this->getMock('CakeResponse', array('statusCode', '_sendHeader')); - $ErrorHandler->controller->response->expects($this->once()) - ->method('statusCode') - ->with($code); - - ob_start(); - $ErrorHandler->render(); - $result = ob_get_clean(); - - foreach ($patterns as $pattern) { - $this->assertPattern($pattern, $result); - } - } } diff --git a/cake/tests/cases/libs/exception_renderer.test.php b/cake/tests/cases/libs/exception_renderer.test.php new file mode 100644 index 000000000..c99c6b118 --- /dev/null +++ b/cake/tests/cases/libs/exception_renderer.test.php @@ -0,0 +1,573 @@ + + * Copyright 2005-2010, Cake Software Foundation, Inc. (http://cakefoundation.org) + * + * Licensed under The MIT License + * Redistributions of files must retain the above copyright notice + * + * @copyright Copyright 2005-2010, Cake Software Foundation, Inc. (http://cakefoundation.org) + * @link http://book.cakephp.org/view/1196/Testing CakePHP(tm) Tests + * @package cake + * @subpackage cake.tests.cases.libs + * @since CakePHP(tm) v 2.0 + * @license MIT License (http://www.opensource.org/licenses/mit-license.php) + */ + +App::import('Core', array('ExceptionRenderer', 'Controller', 'Component')); + +/** + * Short description for class. + * + * @package cake + * @subpackage cake.tests.cases.libs + */ +class AuthBlueberryUser extends CakeTestModel { + +/** + * name property + * + * @var string 'AuthBlueberryUser' + * @access public + */ + public $name = 'AuthBlueberryUser'; + +/** + * useTable property + * + * @var string + * @access public + */ + public $useTable = false; +} + +/** + * BlueberryComponent class + * + * @package cake + * @subpackage cake.tests.cases.libs + */ +class BlueberryComponent extends Component { + +/** + * testName property + * + * @access public + * @return void + */ + public $testName = null; + +/** + * initialize method + * + * @access public + * @return void + */ + function initialize(&$controller) { + $this->testName = 'BlueberryComponent'; + } +} + +/** + * TestErrorController class + * + * @package cake + * @subpackage cake.tests.cases.libs + */ +class TestErrorController extends Controller { + +/** + * uses property + * + * @var array + * @access public + */ + public $uses = array(); + +/** + * components property + * + * @access public + * @return void + */ + public $components = array('Blueberry'); + +/** + * beforeRender method + * + * @access public + * @return void + */ + function beforeRender() { + echo $this->Blueberry->testName; + } + +/** + * index method + * + * @access public + * @return void + */ + function index() { + $this->autoRender = false; + return 'what up'; + } +} + +/** + * MyCustomExceptionRenderer class + * + * @package cake + * @subpackage cake.tests.cases.libs + */ +class MyCustomExceptionRenderer extends ExceptionRenderer { + +/** + * custom error message type. + * + * @return void + */ + function missingWidgetThing() { + echo 'widget thing is missing'; + } +} +/** + * Exception class for testing app error handlers and custom errors. + * + * @package cake.test.cases.libs + */ +class MissingWidgetThingException extends NotFoundException { } + + +/** + * ExceptionRendererTest class + * + * @package cake + * @subpackage cake.tests.cases.libs + */ +class ExceptionRendererTest extends CakeTestCase { + + var $_restoreError = false; +/** + * setup create a request object to get out of router later. + * + * @return void + */ + function setUp() { + App::build(array( + 'views' => array( + TEST_CAKE_CORE_INCLUDE_PATH . 'tests' . DS . 'test_app' . DS . 'views'. DS, + TEST_CAKE_CORE_INCLUDE_PATH . 'libs' . DS . 'view' . DS + ) + ), true); + Router::reload(); + + $request = new CakeRequest(null, false); + $request->base = ''; + Router::setRequestInfo($request); + $this->_debug = Configure::read('debug'); + $this->_error = Configure::read('Error'); + Configure::write('debug', 2); + } + +/** + * teardown + * + * @return void + */ + function teardown() { + Configure::write('debug', $this->_debug); + Configure::write('Error', $this->_error); + App::build(); + if ($this->_restoreError) { + restore_error_handler(); + } + } + +/** + * Mocks out the response on the ExceptionRenderer object so headers aren't modified. + * + * @return void + */ + protected function _mockResponse($error) { + $error->controller->response = $this->getMock('CakeResponse', array('_sendHeader')); + return $error; + } + +/** + * test that methods declared in an ExceptionRenderer subclass are not converted + * into error400 when debug > 0 + * + * @return void + */ + function testSubclassMethodsNotBeingConvertedToError() { + Configure::write('debug', 2); + + $exception = new MissingWidgetThingException('Widget not found'); + $ExceptionRenderer = $this->_mockResponse(new MyCustomExceptionRenderer($exception)); + + ob_start(); + $ExceptionRenderer->render(); + $result = ob_get_clean(); + + $this->assertEqual($result, 'widget thing is missing'); + } + +/** + * test that subclass methods are not converted when debug = 0 + * + * @return void + */ + function testSubclassMethodsNotBeingConvertedDebug0() { + Configure::write('debug', 0); + $exception = new MissingWidgetThingException('Widget not found'); + $ExceptionRenderer = $this->_mockResponse(new MyCustomExceptionRenderer($exception)); + + $this->assertEqual('missingWidgetThing', $ExceptionRenderer->method); + + ob_start(); + $ExceptionRenderer->render(); + $result = ob_get_clean(); + + $this->assertEqual($result, 'widget thing is missing', 'Method declared in subclass converted to error400'); + } + +/** + * test that ExceptionRenderer subclasses properly convert framework errors. + * + * @return void + */ + function testSubclassConvertingFrameworkErrors() { + Configure::write('debug', 0); + + $exception = new MissingControllerException('PostsController'); + $ExceptionRenderer = $this->_mockResponse(new MyCustomExceptionRenderer($exception)); + + $this->assertEqual('error400', $ExceptionRenderer->method); + + ob_start(); + $ExceptionRenderer->render(); + $result = ob_get_clean(); + + $this->assertPattern('/Not Found/', $result, 'Method declared in error handler not converted to error400. %s'); + } + +/** + * test things in the constructor. + * + * @return void + */ + function testConstruction() { + $exception = new NotFoundException('Page not found'); + $ExceptionRenderer = new ExceptionRenderer($exception); + + $this->assertType('CakeErrorController', $ExceptionRenderer->controller); + $this->assertEquals('error400', $ExceptionRenderer->method); + $this->assertEquals($exception, $ExceptionRenderer->error); + } + +/** + * test that method gets coerced when debug = 0 + * + * @return void + */ + function testErrorMethodCoercion() { + Configure::write('debug', 0); + $exception = new MissingActionException('Page not found'); + $ExceptionRenderer = new ExceptionRenderer($exception); + + $this->assertType('CakeErrorController', $ExceptionRenderer->controller); + $this->assertEquals('error400', $ExceptionRenderer->method); + $this->assertEquals($exception, $ExceptionRenderer->error); + } + +/** + * test that unknown exception types with valid status codes are treated correctly. + * + * @return void + */ + function testUnknownExceptionTypeWithExceptionThatHasA400Code() { + $exception = new MissingWidgetThingException('coding fail.'); + $ExceptionRenderer = new ExceptionRenderer($exception); + $ExceptionRenderer->controller->response = $this->getMock('CakeResponse', array('statusCode', '_sendHeader')); + $ExceptionRenderer->controller->response->expects($this->once())->method('statusCode')->with(404); + + ob_start(); + $ExceptionRenderer->render(); + $results = ob_get_clean(); + + $this->assertFalse(method_exists($ExceptionRenderer, 'missingWidgetThing'), 'no method should exist.'); + $this->assertEquals('error400', $ExceptionRenderer->method, 'incorrect method coercion.'); + } + +/** + * test that unknown exception types with valid status codes are treated correctly. + * + * @return void + */ + function testUnknownExceptionTypeWithNoCodeIsA500() { + $exception = new OutOfBoundsException('foul ball.'); + $ExceptionRenderer = new ExceptionRenderer($exception); + $ExceptionRenderer->controller->response = $this->getMock('CakeResponse', array('statusCode', '_sendHeader')); + $ExceptionRenderer->controller->response->expects($this->once())->method('statusCode')->with(500); + + ob_start(); + $ExceptionRenderer->render(); + $results = ob_get_clean(); + + $this->assertEquals('error500', $ExceptionRenderer->method, 'incorrect method coercion.'); + } + +/** + * testerror400 method + * + * @access public + * @return void + */ + function testError400() { + Router::reload(); + + $request = new CakeRequest('posts/view/1000', false); + Router::setRequestInfo($request); + + $exception = new NotFoundException('Custom message'); + $ExceptionRenderer = new ExceptionRenderer($exception); + $ExceptionRenderer->controller->response = $this->getMock('CakeResponse', array('statusCode', '_sendHeader')); + $ExceptionRenderer->controller->response->expects($this->once())->method('statusCode')->with(404); + + ob_start(); + $ExceptionRenderer->render(); + $result = ob_get_clean(); + + $this->assertPattern('/

Custom message<\/h2>/', $result); + $this->assertPattern("/'\/posts\/view\/1000'<\/strong>/", $result); + } + +/** + * test that error400 only modifies the messages on CakeExceptions. + * + * @return void + */ + function testerror400OnlyChangingCakeException() { + Configure::write('debug', 0); + + $exception = new NotFoundException('Custom message'); + $ExceptionRenderer = $this->_mockResponse(new ExceptionRenderer($exception)); + + ob_start(); + $ExceptionRenderer->render(); + $result = ob_get_clean(); + $this->assertContains('Custom message', $result); + + $exception = new MissingActionException(array('controller' => 'PostsController', 'action' => 'index')); + $ExceptionRenderer = $this->_mockResponse(new ExceptionRenderer($exception)); + + ob_start(); + $ExceptionRenderer->render(); + $result = ob_get_clean(); + $this->assertContains('Not Found', $result); + } +/** + * test that error400 doesn't expose XSS + * + * @return void + */ + function testError400NoInjection() { + Router::reload(); + + $request = new CakeRequest('pages/pink', false); + Router::setRequestInfo($request); + + $exception = new NotFoundException('Custom message'); + $ExceptionRenderer = $this->_mockResponse(new ExceptionRenderer($exception)); + + ob_start(); + $ExceptionRenderer->render(); + $result = ob_get_clean(); + + $this->assertNoPattern('##', $result); + } + +/** + * testError500 method + * + * @access public + * @return void + */ + function testError500Message() { + $exception = new InternalErrorException('An Internal Error Has Occurred'); + $ExceptionRenderer = new ExceptionRenderer($exception); + $ExceptionRenderer->controller->response = $this->getMock('CakeResponse', array('statusCode', '_sendHeader')); + $ExceptionRenderer->controller->response->expects($this->once())->method('statusCode')->with(500); + + ob_start(); + $ExceptionRenderer->render(); + $result = ob_get_clean(); + + $this->assertPattern('/

An Internal Error Has Occurred<\/h2>/', $result); + } + +/** + * testMissingController method + * + * @access public + * @return void + */ + function testMissingController() { + $exception = new MissingControllerException(array('controller' => 'PostsController')); + $ExceptionRenderer = $this->_mockResponse(new ExceptionRenderer($exception)); + + ob_start(); + $ExceptionRenderer->render(); + $result = ob_get_clean(); + + $this->assertPattern('/

Missing Controller<\/h2>/', $result); + $this->assertPattern('/PostsController<\/em>/', $result); + } + +/** + * Returns an array of tests to run for the various CakeException classes. + * + * @return void + */ + public static function testProvider() { + return array( + array( + new MissingActionException(array('controller' => 'PostsController', 'action' => 'index')), + array( + '/

Missing Method in PostsController<\/h2>/', + '/PostsController::<\/em>index\(\)<\/em>/' + ), + 404 + ), + array( + new PrivateActionException(array('controller' => 'PostsController' , 'action' => '_secretSauce')), + array( + '/

Private Method in PostsController<\/h2>/', + '/PostsController::<\/em>_secretSauce\(\)<\/em>/' + ), + 404 + ), + array( + new MissingTableException(array('table' => 'articles', 'class' => 'Article')), + array( + '/

Missing Database Table<\/h2>/', + '/table articles<\/em> for model Article<\/em>/' + ), + 500 + ), + array( + new MissingDatabaseException(array('connection' => 'default')), + array( + '/

Missing Database Connection<\/h2>/', + '/Confirm you have created the file/' + ), + 500 + ), + array( + new MissingViewException(array('file' => '/posts/about.ctp')), + array( + "/posts\/about.ctp/" + ), + 500 + ), + array( + new MissingLayoutException(array('file' => 'layouts/my_layout.ctp')), + array( + "/Missing Layout/", + "/layouts\/my_layout.ctp/" + ), + 500 + ), + array( + new MissingConnectionException(array('class' => 'Article')), + array( + '/

Missing Database Connection<\/h2>/', + '/Article requires a database connection/' + ), + 500 + ), + array( + new MissingHelperFileException(array('file' => 'my_custom.php', 'class' => 'MyCustomHelper')), + array( + '/

Missing Helper File<\/h2>/', + '/Create the class below in file:/', + '/(\/|\\\)my_custom.php/' + ), + 500 + ), + array( + new MissingHelperClassException(array('file' => 'my_custom.php', 'class' => 'MyCustomHelper')), + array( + '/

Missing Helper Class<\/h2>/', + '/The helper class MyCustomHelper<\/em> can not be found or does not exist./', + '/(\/|\\\)my_custom.php/', + ), + 500 + ), + array( + new MissingBehaviorFileException(array('file' => 'my_custom.php', 'class' => 'MyCustomBehavior')), + array( + '/

Missing Behavior File<\/h2>/', + '/Create the class below in file:/', + '/(\/|\\\)my_custom.php/', + ), + 500 + ), + array( + new MissingBehaviorClassException(array('file' => 'my_custom.php', 'class' => 'MyCustomBehavior')), + array( + '/The behavior class MyCustomBehavior<\/em> can not be found or does not exist./', + '/(\/|\\\)my_custom.php/' + ), + 500 + ), + array( + new MissingComponentFileException(array('file' => 'sidebox.php', 'class' => 'SideboxComponent')), + array( + '/

Missing Component File<\/h2>/', + '/Create the class SideboxComponent<\/em> in file:/', + '/(\/|\\\)sidebox.php/' + ), + 500 + ), + array( + new MissingComponentClassException(array('file' => 'sidebox.php', 'class' => 'SideboxComponent')), + array( + '/

Missing Component Class<\/h2>/', + '/Create the class SideboxComponent<\/em> in file:/', + '/(\/|\\\)sidebox.php/' + ), + 500 + ) + + ); + } + +/** + * Test the various CakeException sub classes + * + * @dataProvider testProvider + * @return void + */ + function testCakeExceptionHandling($exception, $patterns, $code) { + $ExceptionRenderer = new ExceptionRenderer($exception); + $ExceptionRenderer->controller->response = $this->getMock('CakeResponse', array('statusCode', '_sendHeader')); + $ExceptionRenderer->controller->response->expects($this->once()) + ->method('statusCode') + ->with($code); + + ob_start(); + $ExceptionRenderer->render(); + $result = ob_get_clean(); + + foreach ($patterns as $pattern) { + $this->assertPattern($pattern, $result); + } + } +}