<?php
/**
 * Preferences storage implementation for PHP's LDAP extention.
 *
 * Required parameters:
 * ====================
 *   'basedn'    --  The base DN for the LDAP server.
 *   'hostspec'  --  The hostname of the LDAP server.
 *   'uid'       --  The username search key.
 *
 * Optional parameters:
 * ====================
 *   'password'  --  'rootdn's password for bind authentication.
 *   'port'      --  The port of the LDAP server.
 *                   DEFAULT: 389
 *   'rootdn'    --  The DN of the root (administrative) account to bind for
 *                   write operations.
 *   'username'  --  TODO
 *   'version'   --  The version of the LDAP protocol to use.
 *                   DEFAULT: NONE
 *
 * NOTE: parameter 'username' for $params has been deprecated. Use 'rootdn'.
 *
 *
 * If setting up as the Horde preference handler in conf.php, the following
 * is an example configuration.
 * The schemas needed for ldap are in horde/scripts/ldap.  
 *
 *   $conf['prefs']['driver'] = 'ldap';
 *   $conf['prefs']['params']['hostspec'] = 'localhost';
 *   $conf['prefs']['params']['port'] = '389';
 *   $conf['prefs']['params']['basedn'] = 'dc=example,dc=org';
 *   $conf['prefs']['params']['uid'] = 'mail';
 *
 * The following is valid but would only be necessary if users
 * do NOT have permission to modify their own LDAP accounts.
 *
 *   $conf['prefs']['params']['rootdn'] = 'cn=Manager,dc=example,dc=org';
 *   $conf['prefs']['params']['password'] = 'password';
 *
 *
 * $Horde: horde/lib/Prefs/ldap.php,v 1.69 2003/07/10 22:06:32 slusarz Exp $
 *
 * Copyright 1999-2003 Jon Parise <jon@horde.org>
 *
 * See the enclosed file COPYING for license information (LGPL). If you
 * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
 *
 * @author  Jon Parise <jon@horde.org>
 * @version $Revision: 1.69 $
 * @since   Horde 1.3
 * @package horde.prefs
 */
class Prefs_ldap extends Prefs {

    /**
     * Hash containing connection parameters.
     *
     * @var array $params
     */
    var $params = array();

    /**
     * Handle for the current LDAP connection.
     *
     * @var integer $connection
     */
    var $connection;

    /**
     * Boolean indicating whether or not we're connected to the LDAP server.
     *
     * @var boolean $_connected
     */
    var $_connected = false;

    /**
     * String holding the user's DN.
     *
     * @var string $dn
     */
    var $dn = '';


    /**
     * Constructs a new LDAP preferences object.
     *
     * @access public
     *
     * @param string $user               The user who owns these preferences.
     * @param string $password           The password associated with $user.
     * @param string $scope              The current application scope.
     * @param array $params              A hash containing connection
     *                                   parameters.
     * @param optional boolean $caching  Should caching be used?
     */
    function Prefs_ldap($user, $password, $scope = '',
                        $params = array(), $caching = false)
    {
        if (!Horde::extensionExists('ldap')) {
            Horde::fatal(PEAR::raiseError(_("Prefs_ldap: Required LDAP extension not found."), __FILE__, __LINE__));
        }

        $this->user = $user;
        $this->scope = $scope;
        $this->params = $params;
        $this->caching = $caching;

        /* If a valid server port has not been specified, set the default. */
        if (!isset($this->params['port']) || !is_int($this->params['port'])) {
            $this->params['port'] = 389;
        }

        /* If $params['rootdn'] is empty, authenticate as the current user.
           Note: This assumes the user is allowed to modify their own LDAP
                 entry. */
        if (empty($this->params['username']) &&
            empty($this->params['rootdn'])) {
            $this->params['username'] = $user;
            $this->params['password'] = $password;
        }

        parent::Prefs();
    }

    /**
     * Opens a connection to the LDAP server.
     *
     * @access private
     *
     * @return mixed  True on success or a PEAR_Error object on failure.
     */
    function _connect()
    {
        /* Return if already connected. */
        if ($this->_connected) {
            return true;
        }

        if (!is_array($this->params)) {
            Horde::fatal(PEAR::raiseError(_("No configuration information specified for LDAP Preferences.")), __FILE__, __LINE__);
        }

        $required = array('hostspec', 'basedn', 'uid', 'rootdn', 'password');

        foreach ($required as $val) {
            if (!isset($this->params[$val])) {
                Horde::fatal(PEAR::raiseError(sprintf(_("Required '%s' not specified in preferences configuration."), $val)), __FILE__, __LINE__);
            }
        }

        /* Connect to the LDAP server anonymously. */
        $conn = ldap_connect($this->params['hostspec'], $this->params['port']);
        if (!$conn) {
            Horde::logMessage(
                sprintf('Failed to open an LDAP connection to %s.',
                        $this->params['hostspec']),
                __FILE__, __LINE__);
            return false;
        }

        /* Set the LDAP protocol version. */
        if (isset($this->params['version'])) {
            if (!ldap_set_option($conn, LDAP_OPT_PROTOCOL_VERSION,
                                 $this->params['version'])) {
                Horde::logMessage(
                    sprintf('Set LDAP protocol version to %d failed: [%d] %s',
                            $this->params['version'],
                            ldap_errno($this->connection),
                            ldap_error($this->connection)),
                            __FILE__, __LINE__);
            }
        }

        /* Register our callback function to handle referrals. */
        if (function_exists('ldap_set_rebind_proc') &&
            !ldap_set_rebind_proc($conn, array($this, '_rebind_proc'))) {
            Horde::logMessage(
                sprintf('Set rebind proc failed: [%d] %s',
                        ldap_errno($this->connection),
                        ldap_error($this->connection)),
                __FILE__, __LINE__);
            return false;
        }

        /* Define the DN of the current user */
        $this->dn = sprintf('%s=%s,%s', $this->params['uid'],
                            $this->user,
                            $this->params['basedn']);

        /* And the DN of the authenticating user (may be the same as above) */
        if (!empty($this->params['rootdn'])) {
            $bind_dn = $this->params['rootdn'];
        } else {
            $bind_dn = sprintf('%s=%s,%s', $this->params['uid'],
                               $this->params['username'],
                               $this->params['basedn']);
        }

        /* Store the connection handle at the instance level. */
        $this->connection = $conn;
        $this->_connected = true;

        /* Bind to the LDAP server as the authenticating user. */
        $bind = @ldap_bind($this->connection, $bind_dn,
                           $this->params['password']);
        if (!$bind) {
            Horde::logMessage(
                sprintf('Bind to server %s:%d with DN %s failed: [%d] %s',
                        $this->params['hostspec'],
                        $this->params['port'],
                        $bind_dn,
                        ldap_errno($this->connection),
                        ldap_error($this->connection)),
                __FILE__, __LINE__);
            return false;
        }

        /* Search for the user's full DN. */
        $search = ldap_search($this->connection, $this->params['basedn'],
                              $this->params['uid'] . '=' . $this->user,
                              array('dn'));
        if ($search) {
            $result = ldap_get_entries($this->connection, $search);
            if ($result && !empty($result[0]['dn'])) {
                $this->dn = $result[0]['dn'];
            }
        } else {
            Horde::logMessage(
                sprintf('Failed to retrieve user\'s DN: [%d] %s',
                        ldap_errno($this->connection),
                        ldap_error($this->connection)),
                __FILE__, __LINE__);
            return false;
        }

        return true;
    }

    /**
     * Disconnect from the LDAP server and clean up the connection.
     *
     * @access private
     *
     * @return boolean  True on success, false on failure.
     */
    function _disconnect()
    {
        if ($this->_connected) {
            $this->dn = '';
            $this->_connected = false;
            return ldap_close($this->connection);
        } else {
            return true;
        }
    }

    /**
     * Callback function for LDAP referrals.  This function is called when an
     * LDAP operation returns a referral to an alternate server.
     *
     * @access private
     *
     * @return integer  1 on error, 0 on success.
     *
     * @since Horde 2.1
     */
    function _rebind_proc($conn, $who)
    {
        /* Strip out the hostname we're being redirected to. */
        $who = preg_replace(array('|^.*://|', '|:\d*$|'), '', $who);

        /* Figure out the DN of the authenticating user. */
        if (!empty($this->params['rootdn'])) {
            $bind_dn = $this->params['rootdn'];
        } else {
            $bind_dn = sprintf('%s=%s,%s', $this->params['uid'],
                               $this->params['username'],
                               $this->params['basedn']);
        }

        /* Make sure the server we're being redirected to is in our list of
           valid servers. */
        if (!strstr($this->params['hostspec'], $who)) {
            Horde::logMessage(
                sprintf('Referral target %s for DN %s is not in the authorized server list!', $who, $bind_dn),
                __FILE__, __LINE__);
            return 1;
        }

        /* Bind to the new server. */
        $bind = @ldap_bind($conn, $bind_dn, $this->params['password']);
        if (!$bind) {
            Horde::logMessage(
                sprintf('Rebind to server %s:%d with DN %s failed: [%d] %s',
                        $this->params['hostspec'],
                        $this->params['port'],
                        $bind_dn,
                        ldap_errno($this->connection),
                        ldap_error($this->connection)),
                __FILE__, __LINE__);
        }

        return 0;
    }

    /**
     * Retrieve a value or set of values for a specified user.
     *
     * @access public
     *
     * @param string $user            The user to retrieve prefs for.
     * @param mixed $values           A string or array with the preferences
     *                                to retrieve.
     * @param optional string $scope  The preference scope to look in.
     *                                Defaults to 'horde'.
     *
     * @return mixed  If a single value was requested, the value for that
     *                preference.  Otherwise, a hash, indexed by pref names,
     *                with the requested values.
     *
     * @since Horde 2.2
     */
    function getPref($user, $retrieve, $scope = 'horde')
    {
        /* Make sure we're connected. */
        $this->_connect();

        if (is_array($retrieve)) {
            return array();
        } else {
            return null;
        }
    }

    /**
     * Set a value or set of values for a specified user.
     *
     * @access public
     *
     * @param string $user            The user to set prefs for.
     * @param array  $values          A hash with the preferences
     *                                to set.
     * @param optional string $scope  The preference scope to use.
     *                                Defaults to 'horde'.
     *
     * @return mixed  True on success, a PEAR_Error on failure.
     */
    function setPref($user, $set, $scope = 'horde')
    {
        return PEAR::raiseError('setPref() is not implemented for the LDAP driver');
    }

    /**
     * Retrieves the requested set of preferences from the user's LDAP
     * entry.
     *
     * @access public
     *
     * @param optional array $prefs  An array listing the preferences to
     *                               retrieve. If not specified, retrieve all
     *                               of the preferences listed in the $prefs
     *                               hash.
     *
     * @return mixed  True on success or a PEAR_Error object on failure.
     */
    function retrieve($prefs = array())
    {
        /* If a list of preferences to store hasn't been provided in
           $prefs, assume all preferences are desired. */
        if (!count($prefs)) {
            $prefs = $this->listAll();
        }
        if (!is_array($prefs) || empty($prefs)) {
            return (PEAR::raiseError(_("No preferences are available.")));
        }

        /* Attempt to pull the values from the session cache first. */
        if ($this->cacheLookup($prefs)) {
            return true;
        }

        /* Make sure we are connected. */
        $this->_connect();

        /* Only fetch the fields for the attributes we need. */
        $attrs = array('hordePrefs');
        if (strcmp($this->scope, 'horde') != 0) {
            array_push($attrs, $this->scope . 'Prefs');
        }

        /* Search for the multi-valued field containing the array of
           preferences. */
        $search = ldap_search($this->connection, $this->params['basedn'],
                              $this->params['uid'] . '=' . $this->user, $attrs);
        if ($search) {
            $result = ldap_get_entries($this->connection, $search);
        } else {
            Horde::logMessage('Failed to connect to LDAP preferences server.', __FILE__, __LINE__);
        }

        /* ldap_get_entries() converts attribute indexes to lowercase. */
        $field = String::lower($this->scope . 'prefs');

        if (isset($result)) {
            /* Set the requested values in the $this->_prefs hash based on
               the contents of the LDAP result.

               Preferences are stored as colon-separated name:value pairs.
               Each pair is stored as its own attribute off of the multi-
               value attribute named in: $this->scope . 'Prefs'

               Note that Prefs::setValue() can't be used here because of the
               check for the "changeable" bit.  We want to override that
               check when populating the $this->_prefs hash from the LDAP
               server.
             */

            /* If hordePrefs exists, merge them as the base of the prefs. */
            if (isset($result[0]['hordeprefs'])) {
                $prefs = array_merge($prefs, $result[0]['hordeprefs']);
            }

            /* If this scope's prefs are available, merge them as will.  Give
             * them a higher precedence than hordePrefs. */
            if (strcmp($this->scope, 'horde') != 0) {
                if (isset($result[0][$field])) {
                    $prefs = array_merge($prefs, $result[0][$field]);
                }
            }

            foreach ($prefs as $prefstr) {
                /* If the string doesn't contain a colon delimiter, skip it. */
                if (substr_count($prefstr, ':') == 0) {
                    continue;
                }

                /* Split the string into its name:value components. */
                list($pref, $val) = split(':', $prefstr, 2);

                /* Retrieve this preference. */
                if (isset($this->_prefs[$pref])) {
                    $this->_setValue($pref, base64_decode($val), false);
                    $this->setDefault($pref, false);
                } else {
                    $this->add($pref, base64_decode($val), _PREF_SHARED);
                }
            }

            /* Call hooks. */
            $this->_callHooks();
        } else {
            Horde::logMessage('No preferences were retrieved.', __FILE__, __LINE__);
            return;
        }

        /* Update the session cache. */
        $this->cacheUpdate();

        return true;
    }

    /**
     * Stores preferences to the LDAP server.
     *
     * @access public
     *
     * @param optional array $prefs  An array listing the preferences to be
     *                               stored. If not specified, store all the
     *                               preferences listed in the $prefs hash.
     *
     * @return mixed  True on success or a PEAR_Error object on failure.
     */
    function store($prefs = array())
    {
        $updated = true;

        /* If a list of preferences to store hasn't been provided in
           $prefs, assume all preferences are desired. */
        if (!count($prefs)) {
            $prefs = $this->listAll();
        }
        if (!is_array($prefs) || empty($prefs)) {
            return (PEAR::raiseError(_("No preferences are available.")));
        }

        /* Check for any "dirty" preferences. If no "dirty" preferences are
           found, there's no need to update the LDAP server.
           Exit successfully. */
        if (!($dirty_prefs = $this->_dirtyPrefs($prefs))) {
            return true;
        }

        /* Make sure we are connected. */
        $this->_connect();

        /* Build a hash of the preferences and their values that need
           to be stored in the LDAP server. Because we have to update
           all of the values of a multi-value entry wholesale, we
           can't just pick out the dirty preferences; we must update
           everything.
         */
        $new_values = array();
        foreach ($prefs as $pref) {
            // Don't store locked preferences.
            if (!$this->isLocked($pref)) {
                $entry = $pref . ':' . base64_encode(Prefs::getValue($pref));
                $field = $this->getScope($pref) . 'Prefs';
                $new_values[$field][] = $entry;
            }
        }

        /* Send the hash to the LDAP server. */
        if (ldap_mod_replace($this->connection, $this->dn, $new_values)) {
            foreach ($dirty_prefs as $pref) {
                $this->setDirty($pref, false);
            }
        } else {
            Horde::logMessage(
                sprintf('Unable to modify preferences: [%d] %s',
                        ldap_errno($this->connection),
                        ldap_error($this->connection)),
                __FILE__, __LINE__);
            $updated = false;
        }

        /* Attempt to cache the preferences in the session. */
        $this->cacheUpdate();

        return $updated;
    }

    /**
     * Perform cleanup operations.
     *
     * @access public
     *
     * @param optional boolean $all  Cleanup all Horde preferences.
     */
    function cleanup($all = false)
    {
        /* Close the LDAP connection. */
        $this->_disconnect();

        parent::cleanup($all);
    }

}
