>/D_
Published on

CakePHP session expire bug

Authors
  • avatar
    Name
    Frank
    Twitter

UPDATE: This bug has been fixed.

This auth component / request handler component "bug" drops the 403 header code from an AJAX response when the session has timed out.

Note: this is based on cakePHP version 1.2.4.8284 and may have been fixed already I haven't checked.

The Problem
If the session has timed out before an AJAX request is made the Auth component is invoked and attempts a redirect with header code 403 (Forbidden):

//Line 357 cake/libs/controller/components/auth.php
$controller->redirect(null, 403);

The controller class calls the beforeRedirect() of the Component class:

//Line 542 cake/libs/controller/controller.php
$response = $this->Component->beforeRedirect($this, $url, $status, $exit);

Which in turn calls the beforeRedirect() method of any loaded components:

//Line 146 cake/libs/controller/component.php
$resp = $component->beforeRedirect($controller, $url, $status, $exit);

While the Auth component does not have a beforeRedirect() method if you have the Request Handler component loaded the beforeRedirect() for the Request Handler is invoked which according to the comment block:

Handles (fakes) redirects for Ajax requests using requestAction()

The beforeRedirect() calls requestAction() of the Object class and that is the problem:

//Line 234 cake/libs/controller/components/request_handler.php
echo $this->requestAction($url, array('return'));
$this->_stop();

requestAction() simply calls a controller's method from any location, you can pass a URL or a few limited options but no header code so the 403 header code used at the beginning in the Auth component is dropped. and not returned to the browser. Execution of the script is then immediately stopped.

The Patch
To patch this problem I overloaded the redirect method in AppController, if the status is 403 and the request is via AJAX then I return the header code with an error message.

    /**
     * Overwrite redirect function for 403 header bug from auth component.
     *
     * (non-PHPdoc)
     * @see cake/libs/controller/Controller#redirect($url, $status, $exit)
     */
    function redirect($url, $status = null, $exit = true) {

        if ($status == 403 && $this->RequestHandler->isAjax()) {

            $this->throwAjaxError(403, __('You do not have access to this page. Your session may have expired, please reload the page, log in and try again.', true));
            $this->_stop();
            return;
        }
        else {
            parent::redirect($url, $status = null, $exit = true);
        }
    }

	/**
	 * Return header codes for AJAX errors.
	 *
	 * @param $errorCode
	 * @param $message
	 * @return unknown_type
	 */
	protected function throwAjaxError ($errorCode = 400, $message = null) {

		if ($this->RequestHandler->isAjax() || (isset($this->isAjax) && $this->isAjax == true)) {
			switch ($errorCode) {
				case 400 :
				case 403 :
				    $defaultMessage = 'The request could not be processed because access is forbidden.';
                    header("'HTTP/1.0 403 Forbidden", true, 403);
                    echo ($message == null)?$defaultMessage:$message;
                    break;
				case 408 :
				case 409 :
					$defaultMessage = 'The request could not be processed because of conflict in the request.';
					header("HTTP/1.0 409 Conflict", true, 409);
					echo ($message == null)?$defaultMessage:$message;
					break;
				case 500 :
					break;
			}
			$this->autoRender = false;
			$this->layout = 'ajax';
			Configure::write('debug', 0);
		}
		else {
			throw new Exception('Ajax Error should only be thrown for ajax requests.');
		}
	}

Just for interests sake, those header responses are handled by a single javascript function triggered by the jquery ajaxError event.

/**
 * Handle ajax errors site wide.
 *
 * Possible error codes include:
 * 400 : Bad Request, general use when request cannot be handled by the server
 * 403 : Forbidden, authorisation denied
 * 408 : Request Timeout, timeout on the server
 * 409 : Conflict, exception thrown or similar server side error
 * 500 : Internal Server Error, error server side
 */
KM.ajaxFailureHandler = function() {

	//Handling any ajax errors
	$("#ajax-error-dialog").ajaxError(function(event, XMLHttpRequest, ajaxOptions, thrownError){

		 if (XMLHttpRequest.status == '409' || XMLHttpRequest.status == '403') {

		   //Unbind ajaxStop
		   $(this).unbind('ajaxStop');

		   //Use the ajax-error message dialog to display an error
		   $('#ajax-error-response', this).text(XMLHttpRequest.responseText);

		   $(this).dialog({
				bgiframe: true,
				modal: true,
				buttons: {
					Ok: function() {
						$(this).dialog('close');
					}
				}
			});
		   $(this).dialog('open');
		 }
	});
};