<?php
/**
 * This class provides the interface to the session storage backend.
 *
 * Copyright 2002-2017 Horde LLC (http://www.horde.org/)
 *
 * See the enclosed file COPYING for license information (LGPL). If you
 * did not receive this file, see http://www.horde.org/licenses/lgpl21.
 *
 * @author   Michael Slusarz <slusarz@horde.org>
 * @category Horde
 * @license  http://www.horde.org/licenses/lgpl21 LGPL 2.1
 * @package  SessionHandler
 */
class Horde_SessionHandler
{
    /**
     * If true, indicates the session data has changed.
     *
     * @var boolean
     */
    public $changed = false;

    /**
     * Has a connection been made to the backend?
     *
     * @var boolean
     */
    protected $_connected = false;

    /**
     * A logger instance.
     *
     * @var Horde_Log_Logger
     */
    protected $_logger;

    /**
     * Configuration parameters.
     *
     * @var array
     */
    protected $_params = array();

    /**
     * Initial session data signature.
     *
     * @var string
     */
    protected $_sig;

    /**
     * The storage object.
     *
     * @var Horde_SessionHandler_Storage
     */
    protected $_storage;

    /**
     * Constructor.
     *
     * @param Horde_SessionHandler_Storage $storage  The storage object.
     * @param array $params                          Configuration parameters:
     * <pre>
     *   - logger: (Horde_Log_Logger) A logger instance.
     *             DEFAULT: No logging
     *   - no_md5: (boolean) If true, does not do MD5 signatures of the
     *             session to determine if the session has changed (calling
     *             code is responsible for marking $changed as true when the
     *             session data has changed).
     *             DEFAULT: false
     *   - noset: (boolean) If true, don't set the save handler.
     *            DEFAULT: false
     *   - parse: (callback) A callback function that parses session
     *            information into an array. Is passed the raw session data
     *            as the only argument; expects either false or an array of
     *            session data as a return.
     *            DEFAULT: No
     * </pre>
     */
    public function __construct(Horde_SessionHandler_Storage $storage,
                                array $params = array())
    {
        $params = array_merge($this->_params, $params);

        $this->_logger = isset($params['logger'])
            ? $params['logger']
            : new Horde_Support_Stub();
        unset($params['logger']);

        $this->_params = $params;
        $this->_storage = $storage;

        if (empty($this->_params['noset'])) {
            session_set_save_handler(
                array($this, 'open'),
                array($this, 'close'),
                array($this, 'read'),
                array($this, 'write'),
                array($this, 'destroy'),
                array($this, 'gc')
            );
        }
    }

    /**
     * Destructor.
     */
    public function __destruct()
    {
        /* This is necessary as of PHP 5.0.5 because objects are not available
         * when the write() handler is called at the end of a session
         * access. */
        session_write_close();
    }

    /**
     * Open the backend.
     *
     * @param string $save_path     The path to the session object.
     * @param string $session_name  The name of the session.
     *
     * @return boolean  True on success, false otherwise.
     */
    public function open($save_path = null, $session_name = null)
    {
        if (!$this->_connected) {
            try {
                $this->_storage->open($save_path, $session_name);
            } catch (Horde_SessionHandler_Exception $e) {
                $this->_logger->log($e, 'ERR');
                return false;
            }

            $this->_connected = true;
        }

        return true;
    }

    /**
     * Close the backend.
     *
     * @return boolean  True on success, false otherwise.
     */
    public function close()
    {
        try {
            $this->_storage->close();
        } catch (Horde_SessionHandler_Exception $e) {
            $this->_logger->log($e, 'ERR');
        }

        $this->_connected = false;
        return true;
    }

    /**
     * Read the data for a particular session identifier from the backend.
     * This method should only be called internally by PHP via
     * session_set_save_handler().
     *
     * @param string $id  The session identifier.
     *
     * @return string  The session data.
     */
    public function read($id)
    {
        if (($result = $this->_storage->read($id)) == '') {
            $this->_logger->log('Error retrieving session data (' . $id . ')', 'DEBUG');
        } else {
            $this->_logger->log('Read session data (' . $id . ')', 'DEBUG');
        }

        if (empty($this->_params['no_md5'])) {
            $this->_sig = md5($result);
        }

        return $result;
    }

    /**
     * Write session data to the backend.
     * This method should only be called internally by PHP via
     * session_set_save_handler().
     *
     * @param string $id            The session identifier.
     * @param string $session_data  The session data.
     *
     * @return boolean  True on success, false otherwise.
     */
    public function write($id, $session_data)
    {
        if ($this->changed ||
            (empty($this->_params['no_md5']) &&
             ($this->_sig != md5($session_data)))) {
            if (!$this->_storage->write($id, $session_data)) {
                $this->_logger->log('Failed to write session data (' . $id . ')', 'DEBUG');
                return false;
            }
            $this->_logger->log('Wrote session data (' . $id . ')', 'DEBUG');
        }

        return true;
    }

    /**
     * Destroy the data for a particular session identifier in the backend.
     * This method should only be called internally by PHP via
     * session_set_save_handler().
     *
     * @param string $id  The session identifier.
     *
     * @return boolean  True on success, false otherwise.
     */
    public function destroy($id)
    {
        if ($this->_storage->destroy($id)) {
            $this->_logger->log('Session data destroyed (' . $id . ')', 'DEBUG');
            return true;
        }

        $this->_logger->log('Failed to destroy session data (' . $id . ')', 'DEBUG');
        return false;
    }

    /**
     * Garbage collect stale sessions from the backend.
     * This method should only be called internally by PHP via
     * session_set_save_handler().
     *
     * @param integer $maxlifetime  The maximum age of a session.
     *
     * @return boolean  True on success, false otherwise.
     */
    public function gc($maxlifetime = 300)
    {
        return $this->_storage->gc($maxlifetime);
    }

    /**
     * Get a list of the valid session identifiers.
     *
     * @return array  A list of valid session identifiers.
     * @throws Horde_SessionHandler_Exception
     */
    public function getSessionIDs()
    {
        return $this->_storage->getSessionIDs();
    }

    /**
     * Returns a list of authenticated users and data about their session.
     *
     * @return array  For authenticated users, the sessionid as a key and the
     *                session information as value. If no parsing function
     *                was provided, will always return an empty array.
     * @throws Horde_SessionHandler_Exception
     */
    public function getSessionsInfo()
    {
        $info = array();

        if (empty($this->_params['parse']) ||
            !is_callable($this->_params['parse'])) {
            return $info;
        }

        /* Explicitly do garbage collection call here to make sure session
         * data is correct. */
        $this->gc(ini_get('session.gc_maxlifetime'));

        $sessions = $this->getSessionIDs();

        $this->_storage->readonly = true;

        foreach ($sessions as $id) {
            try {
                $data = $this->read($id);
                $this->close();
            } catch (Horde_SessionHandler_Exception $e) {
                continue;
            }

            $data = call_user_func($this->_params['parse'], $data);
            if ($data !== false) {
                $info[$id] = $data;
            }
        }

        $this->_storage->readonly = false;

        return $info;
    }

}
