Page 1 of 1

redirect causing session not to write

Posted: Tue Dec 26, 2006 4:00 pm
by Luke
I've got this login action in my user controller. It's supposed to write to a database table like this:
sessions:
id
user_id
user_ip
access
data

Code: Select all

public function loginAction()
    {
        $db = Zend::registry('database');
        $config = Zend::registry('config');
        
        $rememberMeDays = $config->user->rememberMeDays;
        
        require_once 'user/form/base.php';
        require_once 'user/form/rules.php';
        
        /**
         * Assign defaults so no notices are thrown
         */
        $this->_view->assign('formAction', 'user/login');
        $this->_view->assign('message', '');
        $this->_view->assign('rememberIsChecked', $rememberme->getChecked() ? 'checked="checked"' : '');
        $this->_view->assign('rememberMeDays', $rememberMeDays);
        
        $request = $this->getRequest();
        $post = new Zend_Filter_Input($request->getPost());
        if ($form->isSubmitted())
        {
            if($form->validate())
            {
                $username = $form->getSubmitValue('username');
                $password = $form->getSubmitValue('password');
                $remember = $form->getSubmitValue('remember');
                
                $auth = new Zend_Auth(new MC2_Auth_Adapter());
                $options = array(
                    'username' => $username,
                    'password' => $password,
                    'remember' => $remember
                );
                $token = $auth->authenticate($options);
                if (!$token->isValid())
                {
                    // Todo: just send them to login page with the message as a param
                    $this->_view->assign('message', $token->getMessage());
                }
                else
                {
                    Zend_Session_Core::regenerateId();
                    $db_session = Zend::registry('db_session');
                    $db_session->setUserId($token->getIdentity());
                    Zend_Session_Core::writeClose();
	            $this->_redirect('user/home'); // If I comment out this line, all works fine. 
                }
            }
        }
        
        $this->_view->assign('formData', $form->toArray());
        $this->_view->assign('loginForm', $this->_view->render('user/form/login.tpl.php'));
        $this->_view->assign('content', $this->_view->render('user/main.tpl.php'));
        $response = $this->getResponse();
        $response->setBody($this->_view->render('layout/main.tpl.php'));
    }
Everything works fine if I comment out the $this->_redirect('user/home'); line, but if I leave it in, the user_id column is null in the table. What in the world could be causing this?

Posted: Tue Dec 26, 2006 4:27 pm
by RobertGonzalez
What happens in the redirect method? And this is somehow changing a value of a var that is set before that particular method is called?

Posted: Tue Dec 26, 2006 4:33 pm
by feyd

Posted: Tue Dec 26, 2006 4:41 pm
by RobertGonzalez
feyd always knows where to go...

I found this comment in the user comments on the session_write_close() page.

Posted: Tue Dec 26, 2006 4:49 pm
by Luke
That was the first thing I tried:

Code: Select all

Zend_Session_Core::writeClose(); // This is called right before the page is redirected
and that method looks like:

Code: Select all

if (self::$_writeClosed) {
            return;
        }

        if ($readonly) {
            self::$_writable = false;
        }

        session_write_close();
        self::$_writeClosed = true;
And then I looked into the redirect method and found that it writes the session before redirecting anyway...

Code: Select all

/**
     * Redirect to another URL
     *
     * Prepends base URL as defined in request object if url is relative by 
     * default; override this behaviour by setting the third argument false.
     *
     * @param string $url
     * @param int $code HTTP response code to use in redirect; defaults to 302, 
     * Found (same as Location header redirect sent by PHP)
     * @param bool $prependBase
     */
    protected function _redirect($url, $code = 302, $prependBase = true)
    {
        if (headers_sent($file, $line)) {
            throw Zend::exception('Zend_Controller_Exception', 'Cannot redirect because headers were already been sent in file ' . $file . ', line ' . $line);
        }

        // prevent header injections
        $url = str_replace(array("\n", "\r"), '', $url);

        // Close session, if started
        if (isset($_SESSION)) {
            session_write_close(); // See right here! 
        }

        // Send HTTP response code
        if (302 != $code && array_key_exists($code, $this->_redirectCodes)) {
            // Valid code. 302s are automatically generated by PHP for Location 
            // headers
            header('HTTP/1.1 ' . $code . ' ' . $this->_redirectCodes[$code]);
        } elseif (302 != $code) {
            throw Zend::exception('Zend_Controller_Exception', 'Invalid response code ("' . $code . '") provided');
        }

        // If relative URL, decide if we should prepend base URL
        if ($prependBase && !preg_match('|^[a-z]+://|', $url)) {
            $request = $this->getRequest();
            if ($request instanceof Zend_Controller_Request_Http) {
                $base = $request->getBaseUrl();
                if (('/' != substr($base, -1)) && ('/' != substr($url, 0, 1))) {
                    $url = $base . '/' . $url;
                } else {
                    $url = $base . $url;
                }
            }
        }

        // redirect
        header("Location: $url");
        exit();
    }

Posted: Tue Dec 26, 2006 4:55 pm
by RobertGonzalez
OK, my next question is then, where in the code is the information writing itself to the table?

Posted: Tue Dec 26, 2006 5:19 pm
by Luke

Code: Select all

<?php
class MC2_Session_Mysql implements Zend_Session_SaveHandler_Interface
{
    
    /**
     * Contains db object
     *
     * @var MC2_Db_Mysql
     */
    private $_db;
    
    /**
     * Contains user id if it exists
     *
     * @var int
     */
    private $_userId = null;
    
    public function __construct(MC2_Db_Mysql $db, $userId = null)
    {
        $this->_db = $db;
        $this->_userId = $userId;
    }
    
    public function open($savepath, $name)
    {
        return $this->_db->getConnection();
    }
    
    public function close()
    {
        /**
         * My database connection is shared with
         * my application, no need to close it
         */
        return true;
    }
    
    public function read($id)
    {
        $result = $this->_db->query("
			SELECT `session`.`data`
			FROM `session`
			WHERE `session`.`id` = '" . $id . "'
        ");
        return (string) $result->get('data');
    }
    
    public function write($id, $data)
    {
        $ip = $_SERVER['REMOTE_ADDR'];
        $data = $this->_db->escape($data);
        $userId = is_null($this->_userId) ? 'NULL' : $this->_userId;
        $result = $this->_db->query("
            REPLACE INTO `session` ( `id` , `user_id` , `user_ip` , `data` , `access` )
            VALUES (
            '" . $id . "', " . $userId . ", '" . $ip . "', '" . $data . "', " . time() . "
            );
		");
        return $result->success();
    }
    
    public function destroy($id)
    {
        $result = $this->_db->query("
			DELETE FROM `session`
			WHERE `session`.`id` = '" . $id . "'
		");
        return $result->success();
    }
    
    public function gc($maxlifetime)
    {
        $old = time() - $maxlifetime;
        $result = $this->_db->query("
			DELETE FROM `session`
			WHERE `session`.`access` < " . $old . "
		");
        return $result->success();
    }
    
    public function setUserId($id)
    {
        $this->_userId = (integer) $id;
    }
}
?>