Import Horde_Prefs from CVS HEAD
authorMichael M Slusarz <slusarz@curecanti.org>
Mon, 23 Nov 2009 09:00:42 +0000 (02:00 -0700)
committerMichael M Slusarz <slusarz@curecanti.org>
Mon, 23 Nov 2009 20:33:08 +0000 (13:33 -0700)
17 files changed:
framework/Core/lib/Horde/Registry.php
framework/Kolab_Format/test/Horde/Kolab/Format/ContactTest.php
framework/Mime/lib/Horde/Mime/Mdn.php
framework/Prefs/lib/Horde/Prefs.php [new file with mode: 0644]
framework/Prefs/lib/Horde/Prefs/CategoryManager.php [new file with mode: 0644]
framework/Prefs/lib/Horde/Prefs/Credentials.php [new file with mode: 0644]
framework/Prefs/lib/Horde/Prefs/File.php [new file with mode: 0644]
framework/Prefs/lib/Horde/Prefs/Identity.php [new file with mode: 0644]
framework/Prefs/lib/Horde/Prefs/Imsp.php [new file with mode: 0644]
framework/Prefs/lib/Horde/Prefs/Kolab.php [new file with mode: 0644]
framework/Prefs/lib/Horde/Prefs/KolabImap.php [new file with mode: 0644]
framework/Prefs/lib/Horde/Prefs/Ldap.php [new file with mode: 0644]
framework/Prefs/lib/Horde/Prefs/Session.php [new file with mode: 0644]
framework/Prefs/lib/Horde/Prefs/Sql.php [new file with mode: 0644]
framework/Prefs/lib/Horde/Prefs/Ui.php [new file with mode: 0644]
framework/Prefs/package.xml [new file with mode: 0644]
framework/Prefs/test/Horde/Prefs/bug_2838.phpt [new file with mode: 0644]

index f3ae178..569fd37 100644 (file)
@@ -1022,20 +1022,18 @@ class Horde_Registry
      */
     public function loadPrefs($app = null)
     {
-        require_once 'Horde/Prefs.php';
-
         if (is_null($app)) {
             $app = $this->getApp();
         }
 
-        /* If there is no logged in user, return an empty Prefs::
+        /* If there is no logged in user, return an empty Horde_Prefs::
          * object with just default preferences. */
         if (!Horde_Auth::getAuth()) {
-            $GLOBALS['prefs'] = Prefs::factory('session', $app, '', '', null, false);
+            $GLOBALS['prefs'] = Horde_Prefs::factory('Session', $app, '', '', null, false);
         } else {
             if (!isset($GLOBALS['prefs']) ||
                 ($GLOBALS['prefs']->getUser() != Horde_Auth::getAuth())) {
-                $GLOBALS['prefs'] = Prefs::factory($GLOBALS['conf']['prefs']['driver'], $app, Horde_Auth::getAuth(), Horde_Auth::getCredential('password'));
+                $GLOBALS['prefs'] = Horde_Prefs::factory($GLOBALS['conf']['prefs']['driver'], $app, Horde_Auth::getAuth(), Horde_Auth::getCredential('password'));
             } else {
                 $GLOBALS['prefs']->retrieve($app);
             }
index c8382ce..45ed208 100644 (file)
@@ -105,14 +105,11 @@ class Horde_Kolab_Format_ContactTest extends PHPUnit_Framework_TestCase
      */
     public function testCategoriesWithPrefs()
     {
-        @include_once 'Horde.php';
-        @include_once 'Horde/Prefs.php';
-
         global $registry, $prefs;
 
-        if (class_exists('Prefs')) {
+        if (class_exists('Horde_Prefs')) {
             $registry = new DummyRegistry();
-            $prefs    = Prefs::singleton('session');
+            $prefs    = Horde_Prefs::singleton('Session');
 
             /* Monkey patch to allw the value to be set. */
             $prefs->_prefs['categories'] = array('v' => '');
@@ -215,4 +212,4 @@ class Horde_Kolab_Format_Xml_Contact_Dummy extends Horde_Kolab_Format_Xml_Contac
                                    0,
                                    array('type' => self::TYPE_DATETIME));
     }
-}
\ No newline at end of file
+}
index be54b59..bf32a7f 100644 (file)
@@ -143,10 +143,8 @@ class Horde_Mime_Mdn
                              $mailparams = array(), $mod = array(),
                              $err = array())
     {
-        require_once 'Horde/Identity.php';
-
         /* Set up some variables we use later. */
-        $identity = Identity::singleton();
+        $identity = Horde_Prefs_Identity::singleton();
         $from_addr = $identity->getDefaultFromAddress();
 
         $to = $this->getMdnReturnAddr();
diff --git a/framework/Prefs/lib/Horde/Prefs.php b/framework/Prefs/lib/Horde/Prefs.php
new file mode 100644 (file)
index 0000000..e2c9f54
--- /dev/null
@@ -0,0 +1,872 @@
+<?php
+/**
+ * The Horde_Prefs:: class provides a common abstracted interface into the
+ * various preferences storage mediums.  It also includes all of the
+ * functions for retrieving, storing, and checking preference values.
+ *
+ * TODO: document the format of the $_prefs hash here
+ *
+ * $_prefs[*pref name*] = array(
+ *     'value'  => *Default value*,
+ *     'locked' => *boolean*,
+ *     'shared' => *boolean*,
+ *     'type'   => 'checkbox'
+ *                 'text'
+ *                 'password'
+ *                 'textarea'
+ *                 'select'
+ *                 'number'
+ *                 'implicit'
+ *                 'special'
+ *                 'link' - There must be a field named either 'url'
+ *                          (internal application link) or 'xurl'
+ *                          (external application link) if this type is used.
+ *                 'enum'
+ *     'enum'   => TODO,
+ *     'desc'   => _(*Description string*),
+ *     'help'   => *Name of the entry in the XML help file*
+ * );
+ *
+ * Copyright 1999-2009 The Horde Project (http://www.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>
+ * @package Horde_Prefs
+ */
+class Horde_Prefs
+{
+    /** Preference is administratively locked. */
+    const LOCKED = 1;
+
+    /** Preference is shared amongst applications. */
+    const SHARED = 2;
+
+    /** Preference value has been changed. */
+    const DIRTY = 4;
+
+    /** Preference value is the application default.
+     *  DEFAULT is a reserved PHP constant. */
+    const PREFS_DEFAULT = 8;
+
+    /**
+     * Singleton instances.
+     *
+     * @var array
+     */
+    static protected $_instances = array();
+
+    /**
+     * Hash holding the current set of preferences. Each preference is
+     * itself a hash, so this will ultimately be multi-dimensional.
+     *
+     * [*pref name*] => Array(
+     *     [d]  =>  *default value*
+     *     [m]  =>  *pref mask*
+     *     [v]  =>  *pref value*
+     * )
+     *
+     * @var array
+     */
+    protected $_prefs = array();
+
+    /**
+     * String containing the name of the current scope. This is used
+     * to differentiate between sets of preferences (multiple
+     * applications can have a "sortby" preference, for example). By
+     * default, all preferences belong to the "global" (Horde) scope.
+     *
+     * @var string
+     */
+    protected $_scope = 'horde';
+
+    /**
+     * Array of loaded scopes. In order to only load what we need, and
+     * to not load things multiple times, we need to maintain a list
+     * of loaded scopes. $this->_prefs will always be the combination
+     * of the current scope and the 'horde' scope (or just the 'horde'
+     * scope).
+     *
+     * @var array
+     */
+    protected $_scopes = array();
+
+    /**
+     * String containing the current username. This indicates the owner of the
+     * preferences.
+     *
+     * @var string
+     */
+    protected $_user = '';
+
+    /**
+     * Boolean indicating whether preference caching should be used.
+     *
+     * @var boolean
+     */
+    protected $_caching = false;
+
+    /**
+     * Array to cache in. Usually a reference to an array in $_SESSION, but
+     * could be overridden by a subclass for testing or other users.
+     *
+     * @var array
+     */
+    protected $_cache = array();
+
+    /**
+     * Hash holding preferences with hook functions defined.
+     *
+     * @var array
+     */
+    protected $_hooks = array();
+
+    /**
+     * Attempts to return a reference to a concrete instance based on $driver.
+     * It will only create a new instance if no instance with the same
+     * parameters currently exists.
+     *
+     * This should be used if multiple preference sources (and, thus,
+     * multiple instances) are required.
+     *
+     * @param mixed $driver     The type of concrete subclass to return.
+     * @param string $scope     The scope for this set of preferences.
+     * @param string $user      The name of the user who owns this set of
+     *                          preferences.
+     * @param string $password  The password associated with $user.
+     * @param array $params     A hash containing any additional configuration
+     *                          or connection parameters a subclass might need.
+     * @param boolean $caching  Should caching be used?
+     *
+     * @return Horde_Prefs  The concrete reference, or false on an error.
+     * @throws Horde_Exception
+     */
+    static public function singleton($driver, $scope = 'horde', $user = '',
+                                     $password = '', $params = null,
+                                     $caching = true)
+    {
+        if (is_null($params)) {
+            $params = Horde::getDriverConfig('prefs', $driver);
+        }
+
+        $signature = serialize(array($driver, $user, $params, $caching));
+        if (!isset(self::$_instances[$signature])) {
+            self::$_instances[$signature] = self::factory($driver, $scope, $user, $password, $params, $caching);
+        }
+
+        /* Preferences may be cached with a different scope. */
+        self::$_instances[$signature]->setScope($scope);
+
+        return self::$_instances[$signature];
+    }
+
+    /**
+     * Attempts to return a concrete instance based on $driver.
+     *
+     * @param mixed $driver     The type of concrete subclass to return.
+     * @param string $scope     The scope for this set of preferences.
+     * @param string $user      The name of the user who owns this set of
+     *                          preferences.
+     * @param string $password  The password associated with $user.
+     * @param array $params     A hash containing any additional configuration
+     *                          or connection parameters a subclass might need.
+     * @param boolean $caching  Should caching be used?
+     *
+     * @return Horde_Prefs  The newly created concrete instance.
+     * @throws Horde_Exception
+     */
+    static public function factory($driver, $scope = 'horde', $user = '',
+                                   $password = '', $params = null,
+                                   $caching = true)
+    {
+        $driver = ucfirst(basename($driver));
+        if (empty($driver) || $driver == 'None') {
+            $driver = 'Session';
+        }
+
+        $class = 'Horde_Prefs_' . $driver;
+        if (!class_exists($class)) {
+            throw new Horde_Exception('Class definition of ' . $class . ' not found.');
+        }
+
+        if (is_null($params)) {
+            $params = Horde::getDriverConfig('prefs', $driver);
+        }
+
+        /* If $params['user_hook'] is defined, use it to retrieve the value to
+         * use for the username ($this->_user). Otherwise, just use the value
+         * passed in the $user parameter. */
+        if (!empty($params['user_hook']) &&
+            function_exists($params['user_hook'])) {
+            $user = call_user_func($params['user_hook'], $user);
+        }
+
+        $prefs = new $class($scope, $user, $password, $params, $caching);
+        $prefs->retrieve($scope);
+
+        return $prefs;
+    }
+
+    /**
+     * Constructor.
+     *
+     * @param string $scope     The scope for this set of preferences.
+     * @param string $user      The name of the user who owns this set of
+     *                          preferences.
+     * @param string $password  The password associated with $user.
+     * @param array $params     A hash containing any additional configuration
+     *                          or connection parameters a subclass might need.
+     * @param boolean $caching  Should caching be used?
+     *
+     */
+    protected function __construct($scope, $user, $password, $params, $caching)
+    {
+        register_shutdown_function(array($this, 'store'));
+
+        $this->_user = $user;
+        $this->_password = $password;
+        $this->_scope = $scope;
+        $this->_params = $params;
+        $this->_caching = $caching;
+
+        // Create a unique key that's safe to use for caching even if we want
+        // another user's preferences later, then register the cache array in
+        // $_SESSION.
+        if ($this->_caching) {
+            $cacheKey = 'horde_prefs_' . hash('sha1', $this->_user);
+
+            // Store a reference to the $_SESSION array.
+            $this->_cache = &$_SESSION[$cacheKey];
+        }
+    }
+
+    /**
+     * Returns the charset used by the concrete preference backend.
+     *
+     * @return string  The preference backend's charset.
+     */
+    public function getCharset()
+    {
+        return Horde_Nls::getCharset();
+    }
+
+    /**
+     * Return the user who owns these preferences.
+     *
+     * @return string  The user these preferences are for.
+     */
+    public function getUser()
+    {
+        return $this->_user;
+    }
+
+    /**
+     * Get the current scope.
+     *
+     * @return string  The current scope (application).
+     */
+    public function getScope()
+    {
+        return $this->_scope;
+    }
+
+    /**
+     * Change scope without explicitly retrieving preferences.
+     *
+     * @param string $scope  The new scope.
+     */
+    public function setScope($scope)
+    {
+        $this->_scope = $scope;
+    }
+
+    /**
+     * Removes a preference entry from the $prefs hash.
+     *
+     * @param string $pref  The name of the preference to remove.
+     */
+    public function remove($pref)
+    {
+        // FIXME not updated yet.
+        $scope = $this->_getPreferenceScope($pref);
+        unset($this->_prefs[$pref]);
+        unset($this->_cache[$scope][$pref]);
+    }
+
+    /**
+     * Sets the given preferences ($pref) to the specified value
+     * ($val), if the preference is modifiable.
+     *
+     * @param string $pref      The name of the preference to modify.
+     * @param string $val       The new value for this preference.
+     * @param boolean $convert  If true the preference value gets converted
+     *                          from the current charset to the backend's
+     *                          charset.
+     *
+     * @return boolean  True if the value was successfully set, false on a
+     *                  failure.
+     * @throws Horde_Exception
+     */
+    public function setValue($pref, $val, $convert = true)
+    {
+        /* Exit early if this preference is locked or doesn't exist. */
+        if (!isset($this->_prefs[$pref]) || $this->isLocked($pref)) {
+            return false;
+        }
+
+        $result = $this->_setValue($pref, $val, true, $convert);
+        if ($result && $this->isDirty($pref)) {
+            $scope = $this->_getPreferenceScope($pref);
+            $this->_cacheUpdate($scope, array($pref));
+
+            /* If this preference has a change hook, call it now. */
+            try {
+                Horde::callHook('prefs_change_hook_' . $pref, array(), $scope);
+            } catch (Horde_Exception_HookNotSet $e) {}
+        }
+
+        return $result;
+    }
+
+    public function __set($name, $value)
+    {
+        return $this->setValue($name, $value);
+    }
+
+    /**
+     * Sets the given preferences ($pref) to the specified value
+     * ($val), whether or not the preference is user-modifiable, unset
+     * the default bit, and set the dirty bit.
+     *
+     * @param string $pref      The name of the preference to modify.
+     * @param string $val       The new value for this preference.
+     * @param boolean $dirty    True if we should mark the new value as
+     *                          dirty (changed).
+     * @param boolean $convert  If true the preference value gets converted
+     *                          from the current charset to the backend's
+     *                          charset.
+     *
+     * @return boolean  True if the value was successfully set, false on a
+     *                  failure.
+     */
+    protected function _setValue($pref, $val, $dirty = true, $convert = true)
+    {
+        global $conf;
+
+        if ($convert) {
+            $val = $this->convertToDriver($val, Horde_Nls::getCharset());
+        }
+
+        // If the preference's value is already equal to $val, don't
+        // bother changing it. Changing it would set the "dirty" bit,
+        // causing an unnecessary update later.
+        if (isset($this->_prefs[$pref]) &&
+            (($this->_prefs[$pref]['v'] == $val) &&
+             !$this->isDefault($pref))) {
+            return true;
+        }
+
+        // Check to see if the value exceeds the allowable storage
+        // limit.
+        if (isset($GLOBALS['conf']['prefs']['maxsize']) &&
+            (strlen($val) > $GLOBALS['conf']['prefs']['maxsize']) &&
+            isset($GLOBALS['notification'])) {
+            $GLOBALS['notification']->push(sprintf(_("The preference \"%s\" could not be saved because its data exceeded the maximum allowable size"), $pref), 'horde.error');
+            return false;
+        }
+
+        // Assign the new value, unset the "default" bit, and set the
+        // "dirty" bit.
+        if (empty($this->_prefs[$pref]['m'])) {
+            $this->_prefs[$pref]['m'] = 0;
+        }
+        $this->_prefs[$pref]['v'] = $val;
+        $this->setDefault($pref, false);
+        if ($dirty) {
+            $this->setDirty($pref, true);
+        }
+
+        // Finally, copy into the $_scopes array.
+        $this->_scopes[$this->_getPreferenceScope($pref)][$pref] = $this->_prefs[$pref];
+
+        return true;
+    }
+
+    /**
+     * Returns the value of the requested preference.
+     *
+     * @param string $pref      The name of the preference to retrieve.
+     * @param boolean $convert  If true the preference value gets converted
+     *                          from the backend's charset to the current
+     *                          charset.
+     *
+     * @return string  The value of the preference, null if it doesn't exist.
+     */
+    public function getValue($pref, $convert = true)
+    {
+        $value = null;
+
+        if (isset($this->_prefs[$pref]['v'])) {
+            if ($convert) {
+                /* Default values have the current UI charset.
+                 * Stored values have the backend charset. */
+                $value = $this->isDefault($pref)
+                    ? Horde_String::convertCharset($this->_prefs[$pref]['v'], Horde_Nls::getCharset(), Horde_Nls::getCharset())
+                    : $this->convertFromDriver($this->_prefs[$pref]['v'], Horde_Nls::getCharset());
+            } else {
+                $value = $this->_prefs[$pref]['v'];
+            }
+        }
+
+        return $value;
+    }
+
+    public function __get($name)
+    {
+        return $this->getValue($name);
+    }
+
+    /**
+     * Modifies the "locked" bit for the given preference.
+     *
+     * @param string $pref   The name of the preference to modify.
+     * @param boolean $bool  The new boolean value for the "locked" bit.
+     */
+    public function setLocked($pref, $bool)
+    {
+        $this->_setMask($pref, $bool, self::LOCKED);
+    }
+
+    /**
+     * Returns the state of the "locked" bit for the given preference.
+     *
+     * @param string $pref  The name of the preference to check.
+     *
+     * @return boolean  The boolean state of $pref's "locked" bit.
+     */
+    public function isLocked($pref)
+    {
+        return $this->_getMask($pref, self::LOCKED);
+    }
+
+    /**
+     * Modifies the "shared" bit for the given preference.
+     *
+     * @param string $pref   The name of the preference to modify.
+     * @param boolean $bool  The new boolean value for the "shared" bit.
+     */
+    public function setShared($pref, $bool)
+    {
+        $this->_setMask($pref, $bool, self::SHARED);
+    }
+
+    /**
+     * Returns the state of the "shared" bit for the given preference.
+     *
+     * @param string $pref  The name of the preference to check.
+     *
+     * @return boolean  The boolean state of $pref's "shared" bit.
+     */
+    public function isShared($pref)
+    {
+        return $this->_getMask($pref, self::SHARED);
+    }
+
+    /**
+     * Modifies the "dirty" bit for the given preference.
+     *
+     * @param string $pref      The name of the preference to modify.
+     * @param boolean $bool     The new boolean value for the "dirty" bit.
+     */
+    public function setDirty($pref, $bool)
+    {
+        $this->_setMask($pref, $bool, self::DIRTY);
+    }
+
+    /**
+     * Returns the state of the "dirty" bit for the given preference.
+     *
+     * @param string $pref  The name of the preference to check.
+     *
+     * @return boolean  The boolean state of $pref's "dirty" bit.
+     */
+    public function isDirty($pref)
+    {
+        return $this->_getMask($pref, self::DIRTY);
+    }
+
+    /**
+     * Modifies the "default" bit for the given preference.
+     *
+     * @param string $pref   The name of the preference to modify.
+     * @param boolean $bool  The new boolean value for the "default" bit.
+     */
+    public function setDefault($pref, $bool)
+    {
+        $this->_setMask($pref, $bool, self::PREFS_DEFAULT);
+    }
+
+    /**
+     * Returns the default value of the given preference.
+     *
+     * @param string $pref  The name of the preference to get the default for.
+     *
+     * @return string  The preference's default value.
+     */
+    public function getDefault($pref)
+    {
+        return empty($this->_prefs[$pref]['d'])
+            ? ''
+            : $this->_prefs[$pref]['d'];
+    }
+
+    /**
+     * Determines if the current preference value is the default
+     * value from prefs.php or a user defined value
+     *
+     * @param string $pref  The name of the preference to check.
+     *
+     * @return boolean  True if the preference is the application default
+     *                  value.
+     */
+    public function isDefault($pref)
+    {
+        return $this->_getMask($pref, self::PREFS_DEFAULT);
+    }
+
+    /**
+     * Sets the value for a given mask.
+     *
+     * @param string $pref   The name of the preference to modify.
+     * @param boolean $bool  The new boolean value for the "default" bit.
+     * @param integer $mask  The mask to add.
+     */
+    protected function _setMask($pref, $bool, $mask)
+    {
+        if (isset($this->_prefs[$pref]) &&
+            ($bool != $this->_getMask($pref, $mask))) {
+            if ($bool) {
+                $this->_prefs[$pref]['m'] |= $mask;
+            } else {
+                $this->_prefs[$pref]['m'] &= ~$mask;
+            }
+        }
+    }
+
+    /**
+     * Gets the boolean state for a given mask.
+     *
+     * @param string $pref   The name of the preference to modify.
+     * @param integer $mask  The mask to get.
+     *
+     * @return boolean  The boolean state for the given mask.
+     */
+    protected function _getMask($pref, $mask)
+    {
+        return isset($this->_prefs[$pref]['m'])
+            ? (bool)($this->_prefs[$pref]['m'] & $mask)
+            : false;
+    }
+
+    /**
+     * Returns the scope of the given preference.
+     *
+     * @param string $pref  The name of the preference to examine.
+     *
+     * @return string  The scope of the $pref.
+     */
+    protected function _getPreferenceScope($pref)
+    {
+        return $this->isShared($pref) ? 'horde' : $this->_scope;
+    }
+
+    /**
+     * Retrieves preferences for the current scope + the 'horde'
+     * scope.
+     *
+     * @param string $scope  Optional scope specifier - if not present the
+     *                       current scope will be used.
+     */
+    public function retrieve($scope = null)
+    {
+        if (is_null($scope)) {
+            $scope = $this->_scope;
+        } else {
+            $this->_scope = $scope;
+        }
+
+        $this->_loadScope('horde');
+        if ($scope != 'horde') {
+            $this->_loadScope($scope);
+        }
+
+        $this->_prefs = ($scope == 'horde')
+            ? $this->_scopes['horde']
+            : array_merge($this->_scopes['horde'], $this->_scopes[$scope]);
+    }
+
+    /**
+     * Load a specific preference scope.
+     */
+    protected function _loadScope($scope)
+    {
+        // Return if we've already loaded these prefs.
+        if (!empty($this->_scopes[$scope])) {
+            return;
+        }
+
+        // Basic initialization so _something_ is always set.
+        $this->_scopes[$scope] = array();
+
+        // Always set defaults to pick up new default values, etc.
+        $this->_setDefaults($scope);
+
+        // Now check the prefs cache for existing values.
+        if ($this->_cacheLookup($scope)) {
+            return;
+        }
+
+        $this->_retrieve($scope);
+        $this->_callHooks($scope);
+
+        /* Update the session cache. */
+        $this->_cacheUpdate($scope, array_keys($this->_scopes[$scope]));
+    }
+
+    /**
+     * This function will be run at the end of every request as a shutdown
+     * function (registered by the constructor).  All prefs with the
+     * dirty bit set will be saved to the storage backend at this time; thus,
+     * there is no need to manually call $prefs->store() every time a
+     * preference is changed.
+     */
+    public function store()
+    {
+    }
+
+    /**
+     * TODO
+     *
+     * @throws Horde_Exception
+     */
+    protected function _retrieve()
+    {
+    }
+
+    /**
+     * This function provides common cleanup functions for all of the driver
+     * implementations.
+     *
+     * @param boolean $all  Clean up all Horde preferences.
+     */
+    public function cleanup($all = false)
+    {
+        /* Perform a Horde-wide cleanup? */
+        if ($all) {
+            /* Destroy the contents of the preferences hash. */
+            $this->_prefs = array();
+
+            /* Destroy the contents of the preferences cache. */
+            unset($this->_cache);
+        } else {
+            /* Remove this scope from the preferences cache, if it exists. */
+            unset($this->_cache[$this->_scope]);
+        }
+    }
+
+    /**
+     * Clears all preferences from the backend.
+     */
+    public function clear()
+    {
+        $this->cleanup(true);
+    }
+
+    /**
+     * Converts a value from the driver's charset to the specified charset.
+     *
+     * @param mixed $value     A value to convert.
+     * @param string $charset  The charset to convert to.
+     *
+     * @return mixed  The converted value.
+     */
+    public function convertFromDriver($value, $charset)
+    {
+        return $value;
+    }
+
+    /**
+     * Converts a value from the specified charset to the driver's charset.
+     *
+     * @param mixed $value     A value to convert.
+     * @param string $charset  The charset to convert from.
+     *
+     * @return mixed  The converted value.
+     */
+    public function convertToDriver($value, $charset)
+    {
+        return $value;
+    }
+
+    /**
+     * Return all "dirty" preferences across all scopes.
+     *
+     * @return array  The values for all dirty preferences, in a
+     *                multi-dimensional array of scope => pref name =>
+     *                pref values.
+     */
+    protected function _dirtyPrefs()
+    {
+        $dirty_prefs = array();
+
+        foreach ($this->_scopes as $scope => $prefs) {
+            foreach ($prefs as $pref_name => $pref) {
+                if (isset($pref['m']) && ($pref['m'] & self::DIRTY)) {
+                    $dirty_prefs[$scope][$pref_name] = $pref;
+                }
+            }
+        }
+
+        return $dirty_prefs;
+    }
+
+    /**
+     * Updates the session-based preferences cache (if available).
+     *
+     * @param string $scope  The scope of the prefs being updated.
+     * @param array $prefs   The preferences to update.
+     */
+    protected function _cacheUpdate($scope, $prefs)
+    {
+        if ($this->_caching && isset($this->_cache)) {
+            /* Place each preference in the cache according to its
+             * scope. */
+            foreach ($prefs as $name) {
+                if (isset($this->_scopes[$scope][$name])) {
+                    $this->_cache[$scope][$name] = $this->_scopes[$scope][$name];
+                }
+            }
+        }
+    }
+
+    /**
+     * Tries to find the requested preferences in the cache. If they
+     * exist, update the $_scopes hash with the cached values.
+     *
+     * @return boolean  True on success, false on failure.
+     */
+    protected function _cacheLookup($scope)
+    {
+        if ($this->_caching && isset($this->_cache[$scope])) {
+            $this->_scopes[$scope] = $this->_cache[$scope];
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * Populates the $_scopes hash with new entries and externally
+     * defined default values.
+     *
+     * @param string $scope  The scope to load defaults for.
+     */
+    protected function _setDefaults($scope)
+    {
+        /* Read the configuration file. The $_prefs array is assumed to hold
+         * the default values. */
+        try {
+            $result = Horde::loadConfiguration('prefs.php', array('_prefs'), $scope);
+            if (empty($result)) {
+                return;
+            }
+        } catch (Horde_Exception $e) {
+            return;
+        }
+
+        extract($result);
+        if (!isset($_prefs)) {
+            return;
+        }
+
+        foreach ($_prefs as $name => $pref) {
+            if (isset($pref['value']) &&
+                isset($pref['locked']) &&
+                isset($pref['shared']) &&
+                ($pref['type'] != 'link') &&
+                ($pref['type'] != 'special')) {
+                $name = str_replace('.', '_', $name);
+
+                $mask = 0;
+                $mask &= ~self::DIRTY;
+                $mask |= self::PREFS_DEFAULT;
+
+                if ($pref['locked']) {
+                    $mask |= self::LOCKED;
+                }
+
+                if ($pref['shared'] || ($scope == 'horde')) {
+                    $mask |= self::SHARED;
+                    $pref_scope = 'horde';
+                } else {
+                    $pref_scope = $scope;
+                }
+
+                if ($pref['shared'] && isset($this->_scopes[$pref_scope][$name])) {
+                    // This is a shared preference that was already
+                    // retrieved.
+                    $this->_scopes[$pref_scope][$name]['m'] = $mask & ~self::PREFS_DEFAULT;
+                    $this->_scopes[$pref_scope][$name]['d'] = $pref['value'];
+                } else {
+                    $this->_scopes[$pref_scope][$name] = array('v' => $pref['value'], 'm' => $mask, 'd' => $pref['value']);
+                }
+
+                if (!empty($pref['hook'])) {
+                    if (!isset($this->_hooks[$scope])) {
+                        $this->_hooks[$scope] = array();
+                    }
+                    $this->_hooks[$scope][$name] = $pref_scope;
+                }
+            }
+        }
+    }
+
+    /**
+     * After preferences have been loaded, set any locked or empty
+     * preferences that have hooks to the result of the hook.
+     *
+     * @param string $scope  The preferences scope to call hooks for.
+     *
+     * @throws Horde_Exception
+     */
+    protected function _callHooks($scope)
+    {
+        if (empty($this->_hooks[$scope])) {
+            return;
+        }
+
+        foreach ($this->_hooks[$scope] as $name => $pref_scope) {
+            if ($this->_scopes[$pref_scope][$name]['m'] & self::LOCKED ||
+                empty($this->_scopes[$pref_scope][$name]['v']) ||
+                $this->_scopes[$pref_scope][$name]['m'] & self::PREFS_DEFAULT) {
+
+                try {
+                    $val = Horde::callHook('prefs_hook_' . $name, array($this->_user), $scope);
+                } catch (Horde_Exception_HookNotSet $e) {
+                    continue;
+                }
+
+                if ($this->_scopes[$pref_scope][$name]['m'] & self::PREFS_DEFAULT) {
+                    $this->_scopes[$pref_scope][$name]['v'] = $val;
+                } else {
+                    $this->_scopes[$pref_scope][$name]['v'] = $this->convertToDriver($val, Horde_Nls::getCharset());
+                }
+                if (!($this->_scopes[$pref_scope][$name]['m'] & self::LOCKED)) {
+                    $this->_scopes[$pref_scope][$name]['m'] |= self::DIRTY;
+                }
+            }
+        }
+    }
+
+}
diff --git a/framework/Prefs/lib/Horde/Prefs/CategoryManager.php b/framework/Prefs/lib/Horde/Prefs/CategoryManager.php
new file mode 100644 (file)
index 0000000..9e9e77e
--- /dev/null
@@ -0,0 +1,236 @@
+<?php
+/**
+ * Class for handling a list of categories stored in a user's
+ * preferences.
+ *
+ * Copyright 2004-2009 The Horde Project (http://www.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   Chuck Hagenbuch <chuck@horde.org>
+ * @category Horde
+ * @package  Horde_Prefs
+ */
+class Horde_Prefs_CategoryManager
+{
+    /**
+     * Get all categories.
+     */
+    static public function get()
+    {
+        $string = $GLOBALS['prefs']->getValue('categories');
+        if (empty($string)) {
+            return array();
+        }
+
+        $categories = explode('|', $string);
+        asort($categories);
+
+        return $categories;
+    }
+
+    /**
+     * TODO
+     */
+    static public function getSelect($id, $current = null)
+    {
+        $categories = self::get();
+        $colors = self::colors();
+        $fgcolors = self::fgColors();
+
+        $id_html = htmlspecialchars($id);
+        $html = '<select id="' . $id_html . '" name="' . $id_html . '">';
+
+        if (!in_array($current, $categories) && !empty($current)) {
+            $curr_html = htmlspecialchars($current);
+            $html .= '<option value="*new*' . $curr_html . '">'
+                . sprintf(_("Use Current: %s"), $curr_html)
+                . '</option>'
+                . '<option value="" disabled="disabled">- - - - - - - - -</option>';
+        }
+
+        if (!$GLOBALS['prefs']->isLocked('categories')) {
+            $html .= '<option value="*new*">' . _("New Category")
+                . "</option>\n"
+                . '<option value="" disabled="disabled">- - - - - - - - -</option>';
+        }
+
+        // Always add an Unfiled option.
+        $html .= '<option value="" style="background:'
+            . $colors['_unfiled_'] . ';color:' . $fgcolors['_unfiled_'] . '"'
+            . (empty($current) ? ' selected="selected">' : '>')
+            . htmlspecialchars(_("Unfiled")) . '</option>';
+
+        foreach ($categories as $name) {
+            $name_html = htmlspecialchars($name);
+            $html .= '<option value="' . $name_html
+                . '" style="background:' . (isset($colors[$name]) ? $colors[$name] : '#fff')
+                . ';color:' . (isset($fgcolors[$name]) ? $fgcolors[$name] : '#000') . '"'
+                . ($name === $current ? ' selected="selected">' : '>')
+                . $name_html . '</option>';
+        }
+
+        return $html . '</select>';
+    }
+
+    /**
+     * TODO
+     */
+    static public function getJavaScript($formname, $elementname)
+    {
+        $prompt = addslashes(_("Please type the new category name:"));
+        $error = addslashes(_("You must type a new category name."));
+
+        return <<<JAVASCRIPT
+
+<script type="text/javascript">
+<!--
+function checkCategory()
+{
+    if (document.${formname}['$elementname'].value == '*new*') {
+        var category = window.prompt('$prompt', '');
+        if (category != null && category != '') {
+            document.$formname.new_category.value = category;
+        } else {
+            window.alert('$error');
+            return false;
+        }
+    } else if (document.${formname}['$elementname'].value.indexOf('*new*') != -1) {
+        document.$formname.new_category.value = document.${formname}['$elementname'].value.substr(5, document.${formname}['$elementname'].value.length);
+    }
+
+    return true;
+}
+//-->
+</script>
+JAVASCRIPT;
+    }
+
+    /**
+     * Add a new category.
+     *
+     * @param string $category  The name of the category to add.
+     *
+     * @return mixed  False on failure, or the new category's name.
+     */
+    static public function add($category)
+    {
+        if ($GLOBALS['prefs']->isLocked('categories') || empty($category)) {
+            return false;
+        }
+
+        $categories = self::get();
+        if (in_array($category, $categories)) {
+            return $category;
+        }
+
+        $categories[] = $category;
+        $GLOBALS['prefs']->setValue('categories', implode('|', $categories));
+
+        return $category;
+    }
+
+    /**
+     * Delete a category.
+     *
+     * @param string $category  The category to remove.
+     *
+     * @return boolean  True on success, false on failure.
+     */
+    static public function remove($category)
+    {
+        if ($GLOBALS['prefs']->isLocked('categories')) {
+            return false;
+        }
+
+        $categories = self::get();
+
+        $key = array_search($category, $categories);
+        if ($key === false) {
+            return $key;
+        }
+
+        unset($categories[$key]);
+        $GLOBALS['prefs']->setValue('categories', implode('|', $categories));
+
+        // Remove any color settings for $category.
+        $colors = self::colors();
+        unset($colors[$category]);
+        self::setColors($colors);
+
+        return true;
+    }
+
+    /**
+     * Returns the color for each of the user's categories.
+     *
+     * @return array  A list of colors, key is the category name, value is the
+     *                HTML color code.
+     */
+    static public function colors()
+    {
+        /* Default values that can be overridden but must always be
+         * present. */
+        $colors['_default_'] = '#FFFFFF';
+        $colors['_unfiled_'] = '#DDDDDD';
+
+        $pairs = explode('|', $GLOBALS['prefs']->getValue('category_colors'));
+        foreach ($pairs as $pair) {
+            if (!empty($pair)) {
+                list($category, $color) = explode(':', $pair);
+                $colors[$category] = $color;
+            }
+        }
+
+        $colors[''] = $colors['_unfiled_'];
+
+        return $colors;
+    }
+
+    /**
+     * Returns the foreground color for each of the user's categories.
+     *
+     * @return array  A list of colors, key is the category name, value is the
+     *                HTML color code.
+     */
+    static public function fgColors()
+    {
+        $colors = self::colors();
+        $fgcolors = array();
+        foreach ($colors as $name => $color) {
+            $fgcolors[$name] = Horde_Image::brightness($color) < 128 ? '#f6f6f6' : '#000';
+        }
+
+        return $fgcolors;
+    }
+
+    /**
+     * TODO
+     */
+    static public function setColor($category, $color)
+    {
+        $colors = self::colors();
+        $colors[$category] = $color;
+        self::setColors($colors);
+    }
+
+    /**
+     * TODO
+     */
+    static public function setColors($colors)
+    {
+        $pairs = array();
+        foreach ($colors as $category => $color) {
+            if ($color[0] != '#') {
+                $color = '#' . $color;
+            }
+            if (!empty($category)) {
+                $pairs[] = $category . ':' . $color;
+            }
+        }
+
+        $GLOBALS['prefs']->setValue('category_colors', implode('|', $pairs));
+    }
+
+}
diff --git a/framework/Prefs/lib/Horde/Prefs/Credentials.php b/framework/Prefs/lib/Horde/Prefs/Credentials.php
new file mode 100644 (file)
index 0000000..898e680
--- /dev/null
@@ -0,0 +1,160 @@
+<?php
+/**
+ * Class for handling a list of credentials stored in a user's preferences.
+ *
+ * Copyright 2008-2009 The Horde Project (http://www.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   Jan Schneider <jan@horde.org>
+ * @category Horde
+ * @package  Horde_Prefs
+ */
+class Horde_Prefs_Credentials
+{
+    /**
+     * Singleton instance.
+     *
+     * @var Horde_Prefs_Credentials
+     */
+    static protected $_instance = null;
+
+    /**
+     * The Horde application currently processed.
+     *
+     * @see singleton()
+     * @var string
+     */
+    protected $app;
+
+    /**
+     * A list of preference field names and their values.
+     *
+     * @var array
+     */
+    protected $_credentials = array();
+
+    /**
+     * Cache for getCredentials().
+     *
+     * @var array
+     */
+    protected $_credentialsCache = null;
+
+    /**
+     * Constructor.
+     */
+    public function __construct()
+    {
+        $credentials = @unserialize($GLOBALS['prefs']->getValue('credentials'));
+        if ($credentials) {
+            foreach ($credentials as $app => $app_prefs) {
+                foreach ($app_prefs as $name => $value) {
+                    $this->_credentials['credentials[' . $app . '][' . $name . ']'] = $value;
+                }
+            }
+        }
+    }
+
+    /**
+     * Returns a single instance of the Prefs_Credentials class, and sets the
+     * curently processed application.
+     *
+     * @param string $app  The current application.
+     *
+     * @return Prefs_Credentials  A Prefs_Credentials instance.
+     */
+    static public function singleton($app)
+    {
+        if (is_null(self::$_instance)) {
+            self::$_instance = new Horde_Prefs_Credentials();
+        }
+        self::$_instance->app = $app;
+
+        return self::$_instance;
+    }
+
+    /**
+     * Returns a list of available credentials collected from all Horde
+     * applications.
+     *
+     * @return array  A list of Horde applications and their credentials.
+     */
+    public function getCredentials()
+    {
+        if (!is_null($this->_credentialsCache)) {
+            return $this->_credentialsCache;
+        }
+
+        $credentials_prefs = array();
+        foreach ($GLOBALS['registry']->listApps() as $app) {
+            try {
+                $credentials = $GLOBALS['registry']->callAppMethod($app, 'authCredentials');
+            } catch (Horde_Exception $e) {
+                continue;
+            }
+
+            if (!count($credentials)) {
+                continue;
+            }
+
+            $credentials_prefs[$app] = array();
+            foreach ($credentials as $name => $credential) {
+                $pref = 'credentials[' . $app . '][' . $name . ']';
+                $credential['shared'] = true;
+                $credentials_prefs[$app][$pref] = $credential;
+            }
+        }
+
+        return $credentials_prefs;
+    }
+
+    /**
+     * Displays the preference interface for setting all available
+     * credentials.
+     */
+    public function showUi()
+    {
+        $credentials = $this->getCredentials();
+        $vspace = '';
+        foreach ($credentials as $app => $_prefs) {
+            $prefs = Horde_Prefs_Credentials::singleton($app);
+            echo $vspace . '<h2 class="smallheader">';
+            printf(_("%s authentication credentials"),
+                   $GLOBALS['registry']->get('name', $app));
+            echo '</h2>';
+            foreach (array_keys($_prefs) as $pref) {
+                $helplink = empty($_prefs[$pref]['help'])
+                    ? null
+                    : Horde_Help::link(!empty($_prefs[$pref]['shared']) ? 'horde' : $GLOBALS['registry']->getApp(), $_prefs[$pref]['help']);
+                require $GLOBALS['registry']->get('templates') . '/prefs/' . $_prefs[$pref]['type'] . '.inc';
+            }
+            $vspace = '<br />';
+        }
+    }
+
+    /**
+     * Returns the value of a credential for the currently processed
+     * application.
+     *
+     * @see Horde_Prefs::getValue()
+     *
+     * @param string $pref  A credential name.
+     *
+     * @return mixed  The credential's value, either from the user's
+     *                preferences, or from the default value, or null.
+     */
+    public function getValue($pref)
+    {
+        if (isset($this->_credentials[$pref])) {
+            return $this->_credentials[$pref];
+        }
+        $credentials = $this->getCredentials();
+
+        return isset($credentials[$this->app][$pref]['value'])
+            ? $credentials[$this->app][$pref]['value']
+            : null;
+    }
+
+}
diff --git a/framework/Prefs/lib/Horde/Prefs/File.php b/framework/Prefs/lib/Horde/Prefs/File.php
new file mode 100644 (file)
index 0000000..cebba2f
--- /dev/null
@@ -0,0 +1,227 @@
+<?php
+/**
+ * Preferences storage implementation using files in a directory
+ *
+ * Copyright 2008-2009 The Horde Project (http://www.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   Thomas Jarosch <thomas.jarosch@intra2net.com>
+ * @category Horde
+ * @package  Horde_Prefs
+ */
+class Horde_Prefs_File extends Horde_Prefs
+{
+    /**
+     * Current version number of the data format
+     *
+     * @var int
+     */
+    protected $_version = 2;
+
+    /**
+     * Directory to store the preferences
+     *
+     * @var string
+     */
+    protected $_dirname;
+
+    /**
+     * Full path to the current preference file
+     *
+     * @var string
+     */
+    protected $_fullpath;
+
+    /**
+     * Cached unserialized data of all scopes
+     *
+     * @var array
+     */
+    protected $_fileCache = null;
+
+    /**
+     * Constructor.
+     *
+     * @param string $scope     The current preferences scope.
+     * @param string $user      The user who owns these preferences.
+     * @param string $password  The password associated with $user. (Unused)
+     * @param array $params     A hash containing connection parameters.
+     * @param boolean $caching  Should caching be used?
+     */
+    public function __construct($scope, $user, $password, $params, $caching)
+    {
+        parent::__construct($scope, $user, $password, $params, $caching);
+
+        // Sanity check for directory
+        $error = false;
+        if (empty($params['directory']) || !is_dir($params['directory'])) {
+            Horde::logMessage(_("Preference storage directory is not available."), __FILE__, __LINE__, PEAR_LOG_ERR);
+            $error = true;
+        } elseif (!is_writable($params['directory'])) {
+            Horde::logMessage(sprintf(_("Directory %s is not writeable"), $params['directory']), __FILE__, __LINE__, PEAR_LOG_ERR);
+            $error = true;
+        }
+
+        if ($error) {
+            $this->_dirname = null;
+            $this->_fullpath = null;
+
+            if (isset($GLOBALS['notification'])) {
+                $GLOBALS['notification']->push(_("The preferences backend is currently unavailable and your preferences have not been loaded. You may continue to use the system with default settings."));
+            }
+        } else {
+            $this->_dirname = $params['directory'];
+            $this->_fullpath = $this->_dirname . '/' . basename($user) . '.prefs';
+        }
+    }
+
+    /**
+     * Retrieves the requested set of preferences from the current session.
+     *
+     * @param string $scope  Scope specifier.
+     *
+     * @throws Horde_Exception
+     */
+    protected function _retrieve($scope)
+    {
+        if (is_null($this->_dirname)) {
+            return;
+        }
+
+        if (is_null($this->_fileCache)) {
+            // Try to read
+            $this->_fileCache = $this->_readCache();
+            if (is_null($this->_fileCache)) {
+                return;
+            }
+
+            // Check version number. We can call format transformations hooks
+            // in the future.
+            if (!is_array($this->_fileCache) ||
+                !array_key_exists('__file_version', $this->_fileCache) ||
+                !($this->_fileCache['__file_version'] == $this->_version)) {
+                if ($this->_fileCache['__file_version'] == 1) {
+                    $this->transformV1V2();
+                } else {
+                    throw new Horde_Exception(sprintf('Wrong version number found: %s (should be %d)', $this->_fileCache['__file_version'], $this->_version));
+                }
+            }
+        }
+
+        // Check if the scope exists
+        if (empty($scope) || !array_key_exists($scope, $this->_fileCache)) {
+            return;
+        }
+
+        // Merge config values
+        foreach ($this->_fileCache[$scope] as $name => $val) {
+            if (isset($this->_scopes[$scope][$name])) {
+                $this->_scopes[$scope][$name]['v'] = $val;
+                $this->_scopes[$scope][$name]['m'] &= ~self::PREFS_DEFAULT;
+            } else {
+                // This is a shared preference.
+                $this->_scopes[$scope][$name] = array('v' => $val,
+                                                      'm' => 0,
+                                                      'd' => null);
+            }
+        }
+    }
+
+    /**
+     * Read data from disk.
+     *
+     * @return mixed  Data array on success or null on error.
+     */
+    protected function _readCache()
+    {
+        return file_exists($this->_fullpath)
+            ? unserialize(file_get_contents($this->_fullpath))
+            : null;
+    }
+
+    /**
+     * Transforms the broken version 1 format into version 2.
+     */
+    public function transformV1V2()
+    {
+        $version2 = array('__file_version' => 2);
+        foreach ($this->_fileCache as $scope => $prefs) {
+            if ($scope != '__file_version') {
+                foreach ($prefs as $name => $pref) {
+                    /* Default values should not have been stored by the
+                     * driver. They are being set via the prefs.php files. */
+                    if (!($pref['m'] & self::PREFS_DEFAULT)) {
+                        $version2[$scope][$name] = $pref['v'];
+                    }
+                }
+            }
+        }
+        $this->_fileCache = $version2;
+    }
+
+    /**
+     * Write data to disk
+     *
+     * @return boolean  True on success.
+     */
+    protected function _writeCache()
+    {
+        $tmp_file = Horde_Util::getTempFile('PrefsFile', true, $this->_dirname);
+
+        $data = serialize($this->_fileCache);
+
+        if (file_put_contents($tmp_file, $data) === false) {
+            return false;
+        }
+
+        return @rename($tmp_file, $this->_fullpath);
+    }
+
+    /**
+     * Stores preferences in the current session.
+     *
+     * @return boolean  True on success.
+     * @throws Horde_Exception
+     */
+    public function store()
+    {
+        if (is_null($this->_dirname)) {
+            return false;
+        }
+
+        // Get the list of preferences that have changed. If there are
+        // none, no need to hit the backend.
+        $dirty_prefs = $this->_dirtyPrefs();
+        if (!$dirty_prefs) {
+            return true;
+        }
+
+        // Read in all existing preferences, if any.
+        $this->_retrieve('');
+        if (!is_array($this->_fileCache)) {
+            $this->_fileCache = array('__file_version' => $this->_version);
+        }
+
+        // Update all values from dirty scope
+        foreach ($dirty_prefs as $scope => $prefs) {
+            foreach ($prefs as $name => $pref) {
+                // Don't store locked preferences.
+                if (!($this->_scopes[$scope][$name]['m'] & self::LOCKED)) {
+                    $this->_fileCache[$scope][$name] = $pref['v'];
+
+                    // Clean the pref since it was just saved.
+                    $this->_scopes[$scope][$name]['m'] &= ~self::DIRTY;
+                }
+            }
+        }
+
+        if ($this->_writeCache() == false) {
+            throw new Horde_Exception('Write of preferences to %s failed', $this->_filename);
+        }
+
+        return true;
+    }
+
+}
diff --git a/framework/Prefs/lib/Horde/Prefs/Identity.php b/framework/Prefs/lib/Horde/Prefs/Identity.php
new file mode 100644 (file)
index 0000000..8391c66
--- /dev/null
@@ -0,0 +1,579 @@
+<?php
+/**
+ * This class provides an interface to all identities a user might have. Its
+ * methods take care of any site-specific restrictions configured in prefs.php
+ * and conf.php.
+ *
+ * @todo Remove notification and gettext references.
+ *
+ * Copyright 2001-2009 The Horde Project (http://www.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  Jan Schneider <jan@horde.org>
+ * @package Horde_Prefs
+ */
+class Horde_Prefs_Identity
+{
+    /**
+     * Singleton instances.
+     *
+     * @var array
+     */
+    static protected $_instances = array();
+
+    /**
+     * Array containing all the user's identities.
+     *
+     * @var array
+     */
+    protected $_identities = array();
+
+    /**
+     * A pointer to the user's standard identity.
+     * This one is used by the methods returning values if no other one is
+     * specified.
+     *
+     * @var integer
+     */
+    protected $_default = 0;
+
+    /**
+     * The user whose identities these are.
+     *
+     * @var string
+     */
+    protected $_user = null;
+
+    /**
+     * Array containing all of the properties in this identity.
+     *
+     * @var array
+     */
+    protected $_properties = array('id', 'fullname', 'from_addr');
+
+    /**
+     * The prefs object that this Identity points to.
+     *
+     * @var Horde_Prefs
+     */
+    protected $_prefs;
+
+    /**
+     * Constructor.
+     *
+     * Reads all the user's identities from the prefs object or builds a new
+     * identity from the standard values given in prefs.php.
+     *
+     * @param string $user  If specified, we read another user's identities
+     *                      instead of the current user.
+     */
+    public function __construct($user = null)
+    {
+        $this->_user = is_null($user)
+            ? Horde_Auth::getAuth()
+            : $user;
+
+        if ((is_null($user) || $user == Horde_Auth::getAuth()) &&
+            isset($GLOBALS['prefs'])) {
+            $this->_prefs = $GLOBALS['prefs'];
+        } else {
+            $this->_prefs = Horde_Prefs::singleton($GLOBALS['conf']['prefs']['driver'], $GLOBALS['registry']->getApp(), $user, '', null, false);
+            $this->_prefs->retrieve();
+        }
+
+        if (!($this->_identities = @unserialize($this->_prefs->getValue('identities', false)))) {
+            /* Convert identities from the old format. */
+            $this->_identities = @unserialize($this->_prefs->getValue('identities'));
+        } elseif (is_array($this->_identities)) {
+            $this->_identities = $this->_prefs->convertFromDriver($this->_identities, Horde_Nls::getCharset());
+        }
+
+        $this->setDefault($this->_prefs->getValue('default_identity'));
+    }
+
+    /**
+     * Creates a default identity if none exists yet and sets the preferences
+     * up if the identities are locked.
+     */
+    public function init()
+    {
+        if (!is_array($this->_identities) || (count($this->_identities) <= 0)) {
+            foreach ($this->_properties as $key) {
+                $identity[$key] = $this->_prefs->getValue($key);
+            }
+            if (empty($identity['id'])) {
+                $identity['id'] = _("Default Identity");
+            }
+
+            $this->_identities = array($identity);
+            $this->verify(0);
+        }
+
+        if ($this->_prefs->isLocked('default_identity')) {
+            foreach ($this->_properties as $key) {
+                $value = $this->getValue($key);
+                if (is_array($value)) {
+                    $value = implode("\n", $value);
+                }
+                $this->_prefs->setValue($key, $value);
+                $this->_prefs->setDirty($key, false);
+            }
+        }
+    }
+
+    /**
+     * Saves all identities in the prefs backend.
+     */
+    public function save()
+    {
+        $identities = $this->_identities;
+        if (is_array($identities)) {
+            $identities = $this->_prefs->convertToDriver($identities, Horde_Nls::getCharset());
+        }
+
+        $this->_prefs->setValue('identities', serialize($identities), false);
+        $this->_prefs->setValue('default_identity', $this->_default);
+    }
+
+    /**
+     * Adds a new identity to the array of identities.
+     *
+     * @param array $identity  An identity hash to add.
+     *
+     * @return integer  The pointer to the created identity
+     */
+    public function add($identity = array())
+    {
+        $this->_identities[] = $identity;
+        return count($this->_identities) - 1;
+    }
+
+    /**
+     * Returns a complete identity hash.
+     *
+     * @param integer $identity  The identity to retrieve.
+     *
+     * @return array  An identity hash.
+     */
+    public function get($identity = null)
+    {
+        if (is_null($identity) || !isset($this->_identities[$identity])) {
+            $identity = $this->_default;
+        }
+        return $this->_identities[$identity];
+    }
+
+    /**
+     * Removes an identity from the array of identities.
+     *
+     * @param integer $identity  The pointer to the identity to be removed
+     *
+     * @return array  The removed identity.
+     */
+    public function delete($identity)
+    {
+        $deleted = array_splice($this->_identities, $identity, 1);
+        foreach (array_keys($this->_identities) as $id) {
+            if ($this->setDefault($id)) {
+                break;
+            }
+        }
+        $this->save();
+
+        return $deleted;
+    }
+
+    /**
+     * Returns a pointer to the current default identity.
+     *
+     * @return integer  The pointer to the current default identity.
+     */
+    public function getDefault()
+    {
+        return $this->_default;
+    }
+
+    /**
+     * Sets the current default identity.
+     * If the identity doesn't exist, the old default identity stays the same.
+     *
+     * @param integer $identity  The pointer to the new default identity.
+     *
+     * @return boolean  True on success, false on failure.
+     */
+    public function setDefault($identity)
+    {
+        if (isset($this->_identities[$identity])) {
+            $this->_default = $identity;
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * Returns a property from one of the identities. If this value doesn't
+     * exist or is locked, the property is retrieved from the prefs backend.
+     *
+     * @param string $key        The property to retrieve.
+     * @param integer $identity  The identity to retrieve the property from.
+     *
+     * @return mixed  The value of the property.
+     */
+    public function getValue($key, $identity = null)
+    {
+        if (is_null($identity) || !isset($this->_identities[$identity])) {
+            $identity = $this->_default;
+        }
+
+        return (!isset($this->_identities[$identity][$key]) || $this->_prefs->isLocked($key))
+            ? $this->_prefs->getValue($key)
+            : $this->_identities[$identity][$key];
+    }
+
+    /**
+     * Returns an array with the specified property from all existing
+     * identities.
+     *
+     * @param string $key  The property to retrieve.
+     *
+     * @return array  The array with the values from all identities.
+     */
+    public function getAll($key)
+    {
+        $list = array();
+
+        foreach (array_keys($this->_identities) as $identity) {
+            $list[$identity] = $this->getValue($key, $identity);
+        }
+
+        return $list;
+    }
+
+    /**
+     * Sets a property with a specified value.
+     *
+     * @param string $key        The property to set.
+     * @param mixed $val         The value to which the property should be
+     *                           set.
+     * @param integer $identity  The identity to set the property in.
+     *
+     * @return boolean  True on success, false on failure (property was
+     *                  locked).
+     */
+    public function setValue($key, $val, $identity = null)
+    {
+        if (is_null($identity)) {
+            $identity = $this->_default;
+        }
+
+        if (!$this->_prefs->isLocked($key)) {
+            $this->_identities[$identity][$key] = $val;
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * Returns true if all properties are locked and therefore nothing in the
+     * identities can be changed.
+     *
+     * @return boolean  True if all properties are locked, false otherwise.
+     */
+    public function isLocked()
+    {
+        foreach ($this->_properties as $key) {
+            if (!$this->_prefs->isLocked($key)) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Returns true if the given address belongs to one of the identities.
+     *
+     * @param string $key    The identity key to search.
+     * @param string $value  The value to search for in $key.
+     *
+     * @return boolean  True if the $value was found in $key.
+     */
+    public function hasValue($key, $valueA)
+    {
+        $list = $this->getAll($key);
+
+        foreach ($list as $valueB) {
+            if (!empty($valueB) &&
+                strpos(Horde_String::lower($valueA), Horde_String::lower($valueB)) !== false) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Verifies and sanitizes all identity properties.
+     *
+     * @param integer $identity  The identity to verify.
+     *
+     * @return bool|object  True if the properties are valid.
+     * @throws Horde_Exception
+     */
+    public function verify($identity = null)
+    {
+        if (is_null($identity)) {
+            $identity = $this->_default;
+        }
+
+        if (!$this->getValue('id', $identity)) {
+            $this->setValue('id', _("Unnamed"), $identity);
+        }
+
+        /* RFC 2822 [3.2.5] does not allow the '\' character to be used in the
+         * personal portion of an e-mail string. */
+        if (strpos($this->getValue('fullname', $identity), '\\') !== false) {
+            throw new Horde_Exception('You cannot have the \ character in your full name.');
+        }
+
+        /* Prepare email validator */
+        require_once 'Horde/Form.php';
+        $email = new Horde_Form_Type_email();
+        $vars = new Horde_Variables();
+        $var = new Horde_Form_Variable('', 'replyto_addr', $email, false);
+
+        /* Verify From address. */
+        if ($email->isValid($var, $vars, $this->getValue('from_addr', $identity), $error_message)) {
+            return true;
+        }
+
+        throw new Horde_Exception($error_message);
+    }
+
+    /**
+     * Returns the user's full name.
+     *
+     * @param integer $ident  The identity to retrieve the name from.
+     *
+     * @return string  The user's full name, or the user name if it doesn't
+     *                 exist.
+     */
+    public function getName($ident = null)
+    {
+        if (isset($this->_names[$ident])) {
+            return $this->_names[$ident];
+        }
+
+        $this->_names[$ident] = $this->getValue('fullname', $ident);
+        if (!strlen($this->_names[$ident])) {
+            $this->_names[$ident] = $this->_user;
+        }
+
+        return $this->_names[$ident];
+    }
+
+    /**
+     * Generates the from address to use for the default identity.
+     *
+     * @param boolean $fullname  Include the fullname information.
+     *
+     * @return string  The default from address.
+     */
+    public function getDefaultFromAddress($fullname = false)
+    {
+        $from_addr = '';
+
+        if ($fullname) {
+            $name = $this->getValue('fullname');
+            if (!empty($name)) {
+                $from_addr = $name . ' ';
+            }
+        }
+
+        $addr = $this->getValue('from_addr');
+        if (empty($addr)) {
+            $addr = $this->_user;
+            if (empty($from_addr)) {
+                return $addr;
+            }
+        }
+
+        return $from_addr . '<' . $addr . '>';
+    }
+
+    /**
+     * Sends a message to an email address supposed to be added to the
+     * identity.
+     * A message is send to this address containing a link to confirm that the
+     * address really belongs to that user.
+     *
+     * @param integer $id       The identity's ID.
+     * @param string $old_addr  The old From: address.
+     *
+     * @return TODO
+     * @throws Horde_Mime_Exception
+     */
+    public function verifyIdentity($id, $old_addr)
+    {
+        global $conf;
+
+        $hash = base_convert(microtime() . mt_rand(), 10, 36);
+
+        $pref = @unserialize($this->_prefs->getValue('confirm_email', false));
+        $pref = $pref
+            ? $this->_prefs->convertFromDriver($pref, Horde_Nls::getCharset())
+            : array();
+        $pref[$hash] = $this->get($id);
+        $pref = $this->_prefs->convertToDriver($pref, Horde_Nls::getCharset());
+        $this->_prefs->setValue('confirm_email', serialize($pref), false);
+
+        $new_addr = $this->getValue('from_addr', $id);
+        $confirm = Horde_Util::addParameter(Horde::url($GLOBALS['registry']->get('webroot', 'horde') . '/services/confirm.php', true, -1), 'h', $hash, false);
+        $message = sprintf(_("You have requested to add the email address \"%s\" to the list of your personal email addresses.\n\nGo to the following link to confirm that this is really your address:\n%s\n\nIf you don't know what this message means, you can delete it."),
+                           $new_addr,
+                           $confirm);
+
+        $msg_headers = new Horde_Mime_Headers();
+        $msg_headers->addMessageIdHeader();
+        $msg_headers->addUserAgentHeader();
+        $msg_headers->addHeader('Date', date('r'));
+        $msg_headers->addHeader('To', $new_addr);
+        $msg_headers->addHeader('From', $old_addr);
+        $msg_headers->addHeader('Subject', _("Confirm new email address"));
+
+        $body = new Horde_Mime_Part();
+        $body->setType('text/plain');
+        $body->setContents(Horde_String::wrap($message, 76, "\n"));
+        $body->setCharset(Horde_Nls::getCharset());
+
+        $mail_driver = $conf['mailer']['type'];
+        $mail_params = $conf['mailer']['params'];
+        if (($mail_driver == 'smtp') &&
+            $mail_params['auth'] &&
+            empty($mail_params['username'])) {
+            $mail_params['username'] = Horde_Auth::getAuth();
+            $mail_params['password'] = Horde_Auth::getCredential('password');
+        }
+
+        $body->send($new_addr, $msg_headers, $mail_driver, $mail_params);
+
+        return new Horde_Notification_Event(sprintf(_("A message has been sent to \"%s\" to verify that this is really your address. The new email address is activated as soon as you confirm this message."), $new_addr));
+    }
+
+    /**
+     * Checks whether an identity confirmation is valid, and adds the
+     * validated identity.
+     *
+     * @param string $hash  The saved hash of the identity being validated.
+     *
+     * @return array  A message for the user, and the message level.
+     */
+    public function confirmIdentity($hash)
+    {
+        $confirm = $this->_prefs->getValue('confirm_email', false);
+        if (empty($confirm)) {
+            return array(_("There are no email addresses to confirm."), 'horde.message');
+        }
+
+        $confirm = @unserialize($confirm);
+        if (empty($confirm)) {
+            return array(_("There are no email addresses to confirm."), 'horde.message');
+        } elseif (!isset($confirm[$hash])) {
+            return array(_("Email addresses to confirm not found."), 'horde.message');
+        }
+
+        $identity = $this->_prefs->convertFromDriver($confirm[$hash], Horde_Nls::getCharset());
+        $verified = array();
+        foreach ($identity as $key => $value) {
+            if (!$this->_prefs->isLocked($key)) {
+                $verified[$key] = $value;
+            }
+        }
+        $this->add($verified);
+        $this->save();
+        unset($confirm[$hash]);
+        $this->_prefs->setValue('confirm_email', serialize($confirm), false);
+
+        return array(sprintf(_("The email address %s has been added to your identities. You can close this window now."), $verified['from_addr']), 'horde.success');
+    }
+
+    /**
+     * Attempts to return a concrete instance based on $type.
+     *
+     * @param mixed $driver  The type of concrete Identity subclass to return.
+     *                       This is based on the storage driver. The code is
+     *                       dynamically included. If $type is an array, then
+     *                       we will look in $driver[0]/lib/Prefs/Identity/
+     *                       for the subclass implementation named
+     *                       $driver[1].php.
+     * @param string $user   If specified, we read another user's identities
+     *                       instead of the current user.
+     *
+     * @return Horde_Prefs_Identity  The newly created instance.
+     * @throws Horde_Exception
+     */
+    static public function factory($driver = 'None', $user = null)
+    {
+        if (is_array($driver)) {
+            list($app, $driv_name) = $driver;
+            $driver = basename($driv_name);
+        } else {
+            $driver = basename($driver);
+        }
+
+        /* Return a base Identity object if no driver is specified. */
+        if (empty($driver) || (strcasecmp($driver, 'none') == 0)) {
+            $instance = new Horde_Prefs_Identity($user);
+            $instance->init();
+            return $instance;
+        }
+
+        $class = (empty($app) ? 'Horde' : $app) . '_Prefs_Identity';
+
+        if (class_exists($class)) {
+            $instance = new $class($user);
+            $instance->init();
+            return $instance;
+        }
+
+        throw new Horde_Exception('Class definition of ' . $class . ' not found.');
+    }
+
+    /**
+     * Attempts to return a reference to a concrete instance based on
+     * $type. It will only create a new instance if no instance with
+     * the same parameters currently exists.
+     *
+     * This should be used if multiple types of identities (and, thus,
+     * multiple instances) are required.
+     *
+     * This method must be invoked as:
+     *   $var = Horde_Prefs_Identity::singleton()
+     *
+     * @param mixed $type   The type of concrete subclass to return.
+     *                      This is based on the storage driver ($type). The
+     *                      code is dynamically included. If $type is an array,
+     *                      then we will look in $type[0]/lib/Prefs/Identity/
+     *                      for the subclass implementation named
+     *                      $type[1].php.
+     * @param string $user  If specified, we read another user's identities
+     *                      instead of the current user.
+     *
+     * @return Horde_Prefs_Identity  The concrete reference.
+     * @throws Horde_Exception
+     */
+    static public function singleton($type = 'None', $user = null)
+    {
+        $signature = hash('md5', serialize(array($type, $user)));
+        if (!isset(self::$_instances[$signature])) {
+            self::$_instances[$signature] = Horde_Prefs_Identity::factory($type, $user);
+        }
+
+        return self::$_instances[$signature];
+    }
+
+}
diff --git a/framework/Prefs/lib/Horde/Prefs/Imsp.php b/framework/Prefs/lib/Horde/Prefs/Imsp.php
new file mode 100644 (file)
index 0000000..5013111
--- /dev/null
@@ -0,0 +1,157 @@
+<?php
+/**
+ * Preference storage implementation for an IMSP server.
+ *
+ * Copyright 2004-2009 The Horde Project (http://www.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  Michael Rubinsky <mrubinsk@horde.org>
+ * @package Horde_Prefs
+ */
+class Horde_Prefs_Imsp extends Horde_Prefs
+{
+    /**
+     * Handle for the IMSP server connection.
+     *
+     * @var Net_IMSP
+     */
+    protected $_imsp;
+
+    /**
+     * User password.
+     *
+     * @var string
+     */
+    protected $_password;
+
+    /**
+     * Boolean indicating whether or not we're connected to the IMSP server.
+     *
+     * @var boolean
+     */
+    protected $_connected = false;
+
+    /**
+     * Holds the driver specific parameters.
+     *
+     * @var array
+     */
+    protected $_params = array();
+
+    /**
+     * Retrieves the requested set of preferences from the IMSP server.
+     *
+     * @param string $scope  Scope specifier.
+     *
+     * @throws Horde_Exception
+     */
+    protected function _retrieve($scope)
+    {
+        /* Now connect to the IMSP server. */
+        try {
+            $this->_connect();
+        } catch (Horde_Exception $e) {
+            if (empty($_SESSION['prefs_cache']['unavailable'])) {
+                $_SESSION['prefs_cache']['unavailable'] = true;
+                $GLOBALS['notification']->push(_("The preferences backend is currently unavailable and your preferences have not been loaded. You may continue to use the system with default settings."));
+            }
+            return;
+        }
+
+        $prefs = $this->_imsp->get($scope . '.*');
+        if ($prefs instanceof PEAR_Error) {
+            Horde::logMessage($prefs, __FILE__, __LINE__, PEAR_LOG_ERR);
+            return;
+        }
+
+        foreach ($prefs as $name => $val) {
+            $name = str_replace($scope . '.', '', $name);
+            if ($val != '-') {
+                if (isset($this->_scopes[$scope][$name])) {
+                    $this->_scopes[$scope][$name]['v'] = $val;
+                    $this->_scopes[$scope][$name]['m'] &= ~self::PREFS_DEFAULT;
+                } else {
+                    // This is a shared preference.
+                    $this->_scopes[$scope][$name] = array('v' => $val,
+                                                          'm' => 0,
+                                                          'd' => null);
+                }
+            }
+        }
+    }
+
+    /**
+     * Stores all dirty prefs to IMSP server.
+     */
+    public function store()
+    {
+        // Get the list of preferences that have changed. If there are
+        // none, no need to hit the backend.
+        $dirty_prefs = $this->_dirtyPrefs();
+        if (!$dirty_prefs) {
+            return;
+        }
+
+        $this->_connect();
+
+        foreach ($dirty_prefs as $scope => $prefs) {
+            foreach ($prefs as $name => $pref) {
+                // Don't store locked preferences.
+                if ($this->_scopes[$scope][$name]['m'] & self::LOCKED) {
+                    continue;
+                }
+
+                $value = $pref['v'];
+                if (empty($value)) {
+                    $value = '-';
+                }
+
+                $result = $this->_imsp->set($scope . '.' . $name, $value);
+                if ($result instanceof PEAR_Error) {
+                    Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR);
+                    return;
+                }
+
+                // Clean the pref since it was just saved.
+                $this->_scopes[$scope][$name]['m'] &= ~self::DIRTY;
+            }
+
+            // Update the cache for this scope.
+            $this->_cacheUpdate($scope, array_keys($prefs));
+        }
+    }
+
+    /**
+     * Attempts to set up a connection to the IMSP server.
+     *
+     * @throws Horde_Exception
+     */
+    protected function _connect()
+    {
+        if ($this->_connected) {
+            return;
+        }
+
+        $this->_params['username'] = preg_match('/(^.*)@/', $this->_user, $matches)
+            ? $matches[1]
+            : $this->_user;
+        $this->_params['password'] = $this->_password;
+
+        if (isset($this->_params['socket'])) {
+            $this->_params['socket'] = $params['socket'] . 'imsp_' . $this->_params['username'] . '.sck';
+        }
+
+        $this->_imsp = Net_IMSP::factory('Options', $this->_params);
+        $result = $this->_imsp->init();
+        if ($result instanceof PEAR_Error) {
+            Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR);
+            throw new Horde_Exception($result);
+        }
+
+        $this->_imsp->setLogger($GLOBALS['conf']['log']);
+        $this->_connected = true;
+    }
+
+}
diff --git a/framework/Prefs/lib/Horde/Prefs/Kolab.php b/framework/Prefs/lib/Horde/Prefs/Kolab.php
new file mode 100644 (file)
index 0000000..41796aa
--- /dev/null
@@ -0,0 +1,44 @@
+<?php
+/**
+ * Kolab implementation of the Horde preference system. Derives from the
+ * Prefs_ldap LDAP authentication object, and simply provides parameters to it
+ * based on the global Kolab configuration.
+ *
+ * Copyright 2004-2007 Stuart Binge <s.binge@codefusion.co.za>
+ *
+ * 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   Stuart Binge <s.binge@codefusion.co.za>
+ * @category Horde
+ * @package  Horde_Prefs
+ */
+class Horde_Prefs_Kolab extends Horde_Prefs_Ldap
+{
+    /**
+     * Constructor.
+     *
+     * @param string $scope     The current application scope.
+     * @param string $user      The user who owns these preferences.
+     * @param string $password  The password associated with $user.
+     * @param array $params     A hash containing connection parameters.
+     * @param boolean $caching  Should caching be used?
+     */
+    public function __construct($scope, $user, $password, $params, $caching)
+    {
+        require_once 'Horde/Kolab.php';
+        $params = array(
+            'hostspec' => Kolab::getServer('ldap'),
+            'port' => $GLOBALS['conf']['kolab']['ldap']['port'],
+            'version' => '3',
+            'basedn' => $GLOBALS['conf']['kolab']['ldap']['basedn'],
+            'writedn' => 'user',
+            'searchdn' => $GLOBALS['conf']['kolab']['ldap']['phpdn'],
+            'searchpw' => $GLOBALS['conf']['kolab']['ldap']['phppw'],
+            'uid' => 'mail'
+        );
+
+        parent::__construct($scope, $user, $password, $params, $caching);
+    }
+
+}
diff --git a/framework/Prefs/lib/Horde/Prefs/KolabImap.php b/framework/Prefs/lib/Horde/Prefs/KolabImap.php
new file mode 100644 (file)
index 0000000..1cdc1b2
--- /dev/null
@@ -0,0 +1,220 @@
+<?php
+/**
+ * Preferences storage implementation for a Kolab IMAP server.
+ *
+ * Copyright 2007-2009 The Horde Project (http://www.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  Gunnar Wrobel <p@rdus.de>
+ * @package Horde_Prefs
+ */
+class Horde_Prefs_KolabImap extends Horde_Prefs
+{
+    /**
+     * ID of the config default share
+     *
+     * @var string
+     */
+    protected $_share;
+
+    /**
+     * Handle for the current Kolab connection.
+     *
+     * @var Kolab
+     */
+    protected $_connection;
+
+    /**
+     * Opens a connection to the Kolab server.
+     *
+     * @throws Horde_Exception
+     */
+    protected function _connect()
+    {
+        if (isset($this->_connection)) {
+            return;
+        }
+
+        $shares = Horde_Share::singleton('h-prefs');
+        $default = $shares->getDefaultShare();
+        if ($default instanceof PEAR_Error) {
+            Horde::logMessage($default, __FILE__, __LINE__, PEAR_LOG_ERR);
+            throw new Horde_Exception($default);
+        }
+        $this->_share = $default->getName();
+
+        require_once 'Horde/Kolab.php';
+        $connection = new Kolab('h-prefs');
+        if ($connection instanceof PEAR_Error) {
+            Horde::logMessage($connection, __FILE__, __LINE__, PEAR_LOG_ERR);
+            throw new Horde_Exception($connection);
+        }
+
+        $result = $this->_connection->open($this->_share, 1);
+        if ($result instanceof PEAR_Error) {
+            Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR);
+            throw new Horde_Exception($result);
+        }
+
+        $this->_connection = $connection;
+    }
+
+    /**
+     * Retrieves the requested set of preferences from the user's config folder.
+     *
+     * @param string $scope  Scope specifier.
+     *
+     * @throws Horde_Exception
+     */
+    protected function _retrieve($scope)
+    {
+        try {
+            $this->_connect();
+        } catch (Horde_Exception $e) {
+            if (empty($_SESSION['prefs_cache']['unavailable'])) {
+                $_SESSION['prefs_cache']['unavailable'] = true;
+                if (isset($GLOBALS['notification'])) {
+                    $GLOBALS['notification']->push(_("The preferences backend is currently unavailable and your preferences have not been loaded. You may continue to use the system with default settings."));
+                }
+            }
+            return;
+        }
+
+        try {
+            $pref = $this->_getPref($scope);
+        } catch (Horde_Exception $e) {
+            return;
+        }
+
+        if (is_null($pref)) {
+            /* No preferences saved yet */
+            return;
+        }
+
+        foreach ($pref['pref'] as $prefstr) {
+            // If the string doesn't contain a colon delimiter, skip it.
+            if (strpos($prefstr, ':') === false) {
+                continue;
+            }
+
+            // Split the string into its name:value components.
+            list($name, $val) = explode(':', $prefstr, 2);
+            if (isset($this->_scopes[$scope][$name])) {
+                $this->_scopes[$scope][$name]['v'] = base64_decode($val);
+                $this->_scopes[$scope][$name]['m'] &= ~self::PREFS_DEFAULT;
+            } else {
+                // This is a shared preference.
+                $this->_scopes[$scope][$name] = array('v' => base64_decode($val),
+                                                      'm' => 0,
+                                                      'd' => null);
+            }
+        }
+    }
+
+    /**
+     * Retrieves the requested preference from the user's config folder.
+     *
+     * @param string $scope  Scope specifier.
+     *
+     * @return array  The preference value.
+     * @throws Horde_Exception
+     */
+    protected function _getPref($scope)
+    {
+        $this->_connect();
+
+        $prefs = $this->_connection->getObjects();
+        if ($prefs instanceof PEAR_Error) {
+            Horde::logMessage($prefs, __FILE__, __LINE__, PEAR_LOG_ERR);
+            throw new Horde_Exception($prefs);
+        }
+
+        foreach ($prefs as $pref) {
+            if ($pref['application'] == $scope) {
+                return $pref;
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * Stores preferences to the Kolab server.
+     *
+     * @throws Horde_Exception
+     */
+    public function store()
+    {
+        // Get the list of preferences that have changed. If there are
+        // none, no need to hit the backend.
+        $dirty_prefs = $this->_dirtyPrefs();
+        if (!$dirty_prefs) {
+            return;
+        }
+        $dirty_scopes = array_keys($dirty_prefs);
+
+        $this->_connect();
+
+        // Build a hash of the preferences and their values that need
+        // to be stored on the IMAP 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
+        // every scope that has dirty preferences.
+        foreach ($dirty_scopes as $scope) {
+            $new_values = array();
+            foreach ($this->_scopes[$scope] as $name => $pref) {
+                // Don't store locked preferences.
+                if (!($pref['m'] & self::LOCKED)) {
+                    $new_values[] = $name . ':' . base64_encode($pref['v']);
+                }
+            }
+
+            try {
+                $pref = $this->_getPref($scope);
+            } catch (Horde_Exception $e) {
+                return;
+            }
+
+            if (is_null($pref)) {
+                $old_uid = null;
+                $prefs_uid = $this->_connection->_storage->generateUID();
+            } else {
+                $old_uid = $pref['uid'];
+                $prefs_uid = $pref['uid'];
+            }
+
+            $object = array(
+                'uid' => $prefs_uid,
+                'application' => $scope,
+                'pref' => $new_values
+            );
+
+            $result = $this->_connection->_storage->save($object, $old_uid);
+            if ($result instanceof PEAR_Error) {
+                Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR);
+                return;
+            }
+        }
+
+        // Clean the preferences since they were just saved.
+        foreach ($dirty_prefs as $scope => $prefs) {
+            foreach ($prefs as $name => $pref) {
+                $this->_scopes[$scope][$name]['m'] &= ~_PREF_DIRTY;
+            }
+
+            // Update the cache for this scope.
+            $this->_cacheUpdate($scope, array_keys($prefs));
+        }
+    }
+
+    /**
+     * Clears all preferences from the kolab_imap backend.
+     */
+    public function clear()
+    {
+        return $this->_connection->deleteAll();
+    }
+
+}
diff --git a/framework/Prefs/lib/Horde/Prefs/Ldap.php b/framework/Prefs/lib/Horde/Prefs/Ldap.php
new file mode 100644 (file)
index 0000000..8a0e3d0
--- /dev/null
@@ -0,0 +1,543 @@
+<?php
+/**
+ * Preferences storage implementation for PHP's LDAP extension.
+ *
+ * Required parameters:
+ * 'basedn' - The base DN for the LDAP server.
+ * 'hostspec' - The hostname of the LDAP server.
+ * 'uid' - The username search key.
+ * 'writedn' - One of "user", "admin", or "searchdn"
+ *
+ * Optional parameters:
+ * 'admindn' - The DN of the administrative account to bind for
+ *             write operations.
+ * 'adminpw' - 'admindn's password for bind authentication.
+ * 'port' - The port of the LDAP server.
+ *          DEFAULT: 389
+ * 'searchdn' - The DN of a user with search permissions on the directory
+ * 'searchpw' - 'searchdn's password for binding
+ * 'tls' - Whether to use TLS connections.
+ *         DEFAULT: false
+ * 'version' - The version of the LDAP protocol to use.
+ *             DEFAULT: NONE (system default will be used)
+ *
+ * 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.
+ *
+ * <code>
+ * $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';
+ * </code>
+ *
+ * The following is valid but would only be necessary if users do NOT have
+ * permission to modify their own LDAP accounts.
+ *
+ * <code>
+ * $conf['prefs']['params']['admindn'] = 'cn=Manager,dc=example,dc=org';
+ * $conf['prefs']['params']['adminpw'] = 'password';
+ * </code>
+ *
+ * Copyright 1999-2009 The Horde Project (http://www.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>
+ * @author   Ben Klang <ben@alkaloid.net>
+ * @category Horde
+ * @package  Horde_Prefs
+ */
+class Horde_Prefs_Ldap extends Horde_Prefs
+{
+    /**
+     * Hash containing connection parameters.
+     *
+     * @var array
+     */
+    protected $_params = array();
+
+    /**
+     * Handle for the current LDAP connection.
+     *
+     * @var resource
+     */
+    protected $_connection;
+
+    /**
+     * Boolean indicating whether or not we're connected to the LDAP server.
+     *
+     * @var boolean
+     */
+    protected $_connected = false;
+
+    /**
+     * String holding the user's DN.
+     *
+     * @var string
+     */
+    protected $_dn = '';
+
+    /**
+     * String holding the user's password.
+     *
+     * @var string
+     */
+    protected $_password = '';
+
+    /**
+     * Constructor.
+     *
+     * @param string $scope     The current application scope.
+     * @param string $user      The user who owns these preferences.
+     * @param string $password  The password associated with $user.
+     * @param array $params     A hash containing connection parameters.
+     * @param boolean $caching  Should caching be used?
+     */
+    public function __construct($scope, $user, $password, $params, $caching)
+    {
+        /* If a valid server port has not been specified, set the default. */
+        if (!isset($params['port']) || !is_int($params['port'])) {
+            $params['port'] = 389;
+        }
+
+        parent::__construct($scope, $user, $password, $params, $caching);
+    }
+
+    /**
+     * Opens a connection to the LDAP server.
+     *
+     * @throws Horde_Exception
+     */
+    function _connect()
+    {
+        if ($this->_connected) {
+            return;
+        }
+
+        if (!Horde_Util::extensionExists('ldap')) {
+            throw new Horde_Exception('Required LDAP extension not found.');
+        }
+
+        Horde::assertDriverConfig($this->_params, 'prefs',
+            array('hostspec', 'basedn', 'uid', 'writedn'),
+            'preferences LDAP');
+
+        /* 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__, PEAR_LOG_ERR);
+            throw new Horde_Exception('Internal LDAP error. Details have been logged for the administrator.');
+        }
+
+        /* Set the LDAP protocol version. */
+        if (isset($this->_params['version'])) {
+            $result = @ldap_set_option($conn, LDAP_OPT_PROTOCOL_VERSION,
+                                       $this->_params['version']);
+            if ($result === false) {
+                Horde::logMessage(
+                    sprintf('Set LDAP protocol version to %d failed: [%d] %s',
+                            $this->_params['version'],
+                            @ldap_errno($conn),
+                            @ldap_error($conn)),
+                    __FILE__, __LINE__, PEAR_LOG_WARNING);
+                throw new Horde_Exception('Internal LDAP error. Details have been logged for the administrator.');
+            }
+        }
+
+        /* Start TLS if we're using it. */
+        if (!empty($this->_params['tls'])) {
+            if (!@ldap_start_tls($conn)) {
+                Horde::logMessage(
+                    sprintf('STARTTLS failed: [%d] %s',
+                            @ldap_errno($this->_ds),
+                            @ldap_error($this->_ds)),
+                    __FILE__, __LINE__, PEAR_LOG_ERR);
+            }
+        }
+
+        /* If necessary, bind to the LDAP server as the user with search
+         * permissions. */
+        if (!empty($this->_params['searchdn'])) {
+            $bind = @ldap_bind($conn, $this->_params['searchdn'],
+                               $this->_params['searchpw']);
+            if ($bind === false) {
+                Horde::logMessage(
+                    sprintf('Bind to server %s:%d with DN %s failed: [%d] %s',
+                            $this->_params['hostspec'],
+                            $this->_params['port'],
+                            $this->_params['searchdn'],
+                            @ldap_errno($conn),
+                            @ldap_error($conn)),
+                    __FILE__, __LINE__, PEAR_LOG_ERR);
+                throw new Horde_Exception('Internal LDAP error. Details have been logged for the administrator.');
+            }
+        }
+
+        /* Register our callback function to handle referrals. */
+        if (function_exists('ldap_set_rebind_proc')) {
+            $result = @ldap_set_rebind_proc($conn, array($this, 'rebindProc'));
+            if ($result === false) {
+                Horde::logMessage(
+                    sprintf('Setting referral callback failed: [%d] %s',
+                            @ldap_errno($conn),
+                            @ldap_error($conn)),
+                    __FILE__, __LINE__, PEAR_LOG_WARNING);
+                return PEAR::raiseError(_("Internal LDAP error.  Details have been logged for the administrator."));
+            }
+        }
+
+        /* Store the connection handle at the instance level. */
+        $this->_connection = $conn;
+
+        /* Search for the user's full DN. */
+        $search = @ldap_search($this->_connection, $this->_params['basedn'],
+                               $this->_params['uid'] . '=' . $this->_user, array('dn'));
+        if ($search === false) {
+            Horde::logMessage(
+                sprintf('Error while searching the directory for the user\'s DN: [%d]: %s',
+                        @ldap_errno($this->_connection),
+                        @ldap_error($this->_connection)),
+                __FILE__, __LINE__, PEAR_LOG_ERR);
+            throw new Horde_Exception('Internal LDAP error. Details have been logged for the administrator.');
+        }
+
+        $result = @ldap_get_entries($this->_connection, $search);
+        if ($result === false) {
+            Horde::logMessage(
+                sprintf('Error while retrieving LDAP search results for the user\'s DN: [%d]: %s',
+                        @ldap_errno($this->_connection),
+                        @ldap_error($this->_connection)),
+                __FILE__, __LINE__, PEAR_LOG_ERR);
+            throw new Horde_Exception('Internal LDAP error. Details have been logged for the administrator.');
+        }
+
+        if ($result['count'] != 1) {
+            Horde::logMessage(
+                'Zero or more than one DN returned from search; unable to determine user\'s correct DN.',
+                __FILE__, __LINE__, PEAR_LOG_ERR);
+            throw new Horde_Exception('Internal LDAP error. Details have been logged for the administrator.');
+        }
+        $this->_dn = $result[0]['dn'];
+
+        // Now we should have the user's DN.  Re-bind as appropriate with write
+        // permissions to be able to store preferences.
+        switch($this->_params['writedn']) {
+        case 'user':
+            $result = @ldap_bind($this->_connection,
+                                 $this->_dn, $this->_password);
+            break;
+
+        case 'admin':
+            $result = @ldap_bind($this->_connection,
+                                 $this->_params['admindn'],
+                                 $this->_params['adminpw']);
+            break;
+
+        case 'searchdn':
+            // Since we've already bound as the search DN above, no rebinding
+            // is necessary.
+            $result = true;
+            break;
+        }
+
+        if ($result === false) {
+            Horde::logMessage(
+                sprintf('Error rebinding for prefs writing: [%d]: %s',
+                        @ldap_errno($this->_connection),
+                        @ldap_error($this->_connection)),
+                __FILE__, __LINE__, PEAR_LOG_ERR);
+            throw new Horde_Exception('Internal LDAP error. Details have been logged for the administrator.');
+        }
+
+        // We now have a ready-to-use connection.
+        $this->_connected = true;
+    }
+
+    /**
+     * Callback function for LDAP referrals.  This function is called when an
+     * LDAP operation returns a referral to an alternate server.
+     *
+     * @return integer  1 on error, 0 on success.
+     */
+    public function rebindProc($conn, $who)
+    {
+        /* Strip out the hostname we're being redirected to. */
+        $who = preg_replace(array('|^.*://|', '|:\d*$|'), '', $who);
+
+        /* Make sure the server we're being redirected to is in our list of
+           valid servers. */
+        if (strpos($this->_params['hostspec'], $who) === false) {
+            Horde::logMessage(
+                sprintf('Referral target %s for DN %s is not in the authorized server list.',
+                        $who, $bind_dn),
+                __FILE__, __LINE__, PEAR_LOG_ERR);
+            return 1;
+        }
+
+        /* Figure out the DN of the authenticating user. */
+        switch($this->_params['writedn']) {
+        case 'user':
+            $bind_dn = $this->_dn;
+            $bind_pw = $this->_password;
+            break;
+
+        case 'admin':
+            $bind_dn = $this->_params['admindn'];
+            $bind_pw = $this->_params['adminpw'];
+            break;
+
+        case 'searchdn':
+            $bind_dn = $this->_params['searchdn'];
+            $bind_dn = $this->_params['searchpw'];
+            break;
+        }
+
+        /* Bind to the new server. */
+        $bind = @ldap_bind($conn, $bind_dn, $bind_pw);
+        if ($bind === false) {
+            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__, PEAR_LOG_ERR);
+        }
+
+        return 0;
+    }
+
+    /**
+     * Retrieves the requested set of preferences from the user's LDAP entry.
+     *
+     * @param string $scope  Scope specifier.
+     */
+    function _retrieve($scope)
+    {
+        try {
+            $this->_connect();
+        } catch (Horde_Exception $e) {
+            if (empty($_SESSION['prefs_cache']['unavailable'])) {
+                $_SESSION['prefs_cache']['unavailable'] = true;
+                $GLOBALS['notification']->push(_("The preferences backend is currently unavailable and your preferences have not been loaded. You may continue to use the system with default settings."));
+            }
+            return;
+        }
+
+        // Search for the multi-valued field containing the array of
+        // preferences.
+        $search = @ldap_search($this->_connection, $this->_params['basedn'],
+                              $this->_params['uid'] . '=' . $this->_user,
+                              array($scope . 'Prefs'));
+        if ($search === false) {
+            Horde::logMessage(
+                sprintf('Error while searching for the user\'s prefs: [%d]: %s',
+                        @ldap_errno($this->_connection),
+                        @ldap_error($this->_connection)),
+                __FILE__, __LINE__, PEAR_LOG_ERR);
+            return;
+        }
+
+        $result = @ldap_get_entries($this->_connection, $search);
+        if ($result === false) {
+            Horde::logMessage(
+                sprintf('Error while retrieving LDAP search results for the user\'s prefs: [%d]: %s',
+                        @ldap_errno($this->_connection),
+                        @ldap_error($this->_connection)),
+                __FILE__, __LINE__, PEAR_LOG_ERR);
+            return;
+        }
+
+        // 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: $scope . 'Prefs'
+
+        // ldap_get_entries() converts attribute indexes to lowercase.
+        $field = Horde_String::lower($scope . 'prefs');
+        $prefs = isset($result[0][$field])
+            ? $result[0][$field]
+            : array();
+
+        foreach ($prefs as $prefstr) {
+            // If the string doesn't contain a colon delimiter, skip it.
+            if (strpos($prefstr, ':') === false) {
+                continue;
+            }
+
+            // Split the string into its name:value components.
+            list($name, $val) = explode(':', $prefstr, 2);
+            if (isset($this->_scopes[$scope][$name])) {
+                $this->_scopes[$scope][$name]['v'] = base64_decode($val);
+                $this->_scopes[$scope][$name]['m'] &= ~self::PREFS_DEFAULT;
+            } else {
+                // This is a shared preference.
+                $this->_scopes[$scope][$name] = array('v' => base64_decode($val),
+                                                      'm' => 0,
+                                                      'd' => null);
+            }
+        }
+    }
+
+    /**
+     * Stores preferences to the LDAP server.
+     *
+     * @throws Horde_Exception
+     */
+    public function store()
+    {
+        // Get the list of preferences that have changed. If there are
+        // none, no need to hit the backend.
+        $dirty_prefs = $this->_dirtyPrefs();
+        if (!$dirty_prefs) {
+            return;
+        }
+        $dirty_scopes = array_keys($dirty_prefs);
+
+        $this->_connect();
+
+        // Build a hash of the preferences and their values that need
+        // to be stored on 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
+        // every scope that has dirty preferences.
+        $new_values = array();
+        foreach ($dirty_scopes as $scope) {
+            foreach ($this->_scopes[$scope] as $name => $pref) {
+                // Don't store locked preferences.
+                if (!($pref['m'] & self::LOCKED)) {
+                    $new_values[$scope . 'Prefs'][] =
+                        $name . ':' . base64_encode($pref['v']);
+                }
+            }
+        }
+
+        // Entries must have the objectclasses 'top' and 'hordeperson'
+        // to successfully store LDAP prefs. Check for both of them,
+        // and add them if necessary.
+        $search = @ldap_search($this->_connection, $this->_params['basedn'],
+                              $this->_params['uid'] . '=' . $this->_user,
+                              array('objectclass'));
+        if ($search === false) {
+            Horde::logMessage(
+                sprintf('Error searching the directory for required objectClasses: [%d] %s',
+                        @ldap_errno($this->_connection),
+                        @ldap_error($this->_connection)),
+                __FILE__, __LINE__, PEAR_LOG_ERR);
+            return;
+        }
+
+        $result = @ldap_get_entries($this->_connection, $search);
+        if ($result === false) {
+            Horde::logMessage(
+                sprintf('Error retrieving results while checking for required objectClasses: [%d] %s',
+                        @ldap_errno($this->_connection),
+                        @ldap_error($this->_connection)),
+                __FILE__, __LINE__, PEAR_LOG_ERR);
+            return;
+        }
+
+        if ($result['count'] > 0) {
+            $top = false;
+            $hordeperson = false;
+
+            for ($i = 0; $i < $result[0]['objectclass']['count']; $i++) {
+                if ($result[0]['objectclass'][$i] == 'top') {
+                    $top = true;
+                } elseif ($result[0]['objectclass'][$i] == 'hordePerson') {
+                    $hordeperson = true;
+                }
+            }
+
+            // Add any missing objectclasses.
+            if (!$top) {
+                @ldap_mod_add($this->_connection, $this->_dn, array('objectclass' => 'top'));
+            }
+
+            if (!$hordeperson) {
+                @ldap_mod_add($this->_connection, $this->_dn, array('objectclass' => 'hordePerson'));
+            }
+        }
+
+        // Send the hash to the LDAP server.
+        $result = @ldap_mod_replace($this->_connection, $this->_dn,
+                                    $new_values);
+        if ($result === false) {
+            Horde::logMessage(
+                sprintf('Unable to modify user\'s objectClass for preferences: [%d] %s',
+                        @ldap_errno($this->_connection),
+                        @ldap_error($this->_connection)),
+                __FILE__, __LINE__, PEAR_LOG_ERR);
+            return;
+        }
+
+        // Clean the preferences since they were just saved.
+        foreach ($dirty_prefs as $scope => $prefs) {
+            foreach ($prefs as $name => $pref) {
+                $this->_scopes[$scope][$name]['m'] &= ~_PREF_DIRTY;
+            }
+
+            // Update the cache for this scope.
+            $this->_cacheUpdate($scope, array_keys($prefs));
+        }
+    }
+
+    /**
+     * Clears all preferences from the LDAP backend.
+     *
+     * @throws Horde_Exception
+     */
+    public function clear()
+    {
+        $this->_connect();
+
+        $attrs = $GLOBALS['registry']->listApps(array('inactive', 'active', 'hidden', 'notoolbar', 'admin'));
+        foreach ($attrs as $key => $val) {
+            $attrs[$key] = $val . 'Prefs';
+        }
+
+        $search = @ldap_read($this->_connection, $this->_dn,
+                            'objectClass=hordePerson', $attrs, 1);
+        if ($search === false) {
+            Horde::logMessage(
+                sprintf('Error while getting preferenes from LDAP: [%d] %s',
+                        @ldap_errno($this->_connection),
+                        @ldap_error($this->_connection)),
+                __FILE__, __LINE__, PEAR_LOG_ERR);
+            return;
+        }
+
+        $result = @ldap_get_entries($this->_connection, $search);
+        if ($result === false) {
+            Horde::logMessage(
+                sprintf('Error while retrieving results from LDAP: [%d] %s',
+                        @ldap_errno($this->_connection),
+                        @ldap_error($this->_connection)),
+                __FILE__, __LINE__, PEAR_LOG_ERR);
+            return;
+        }
+
+        $attrs = array();
+        for ($i = 0; $i < $result[0]['count']; $i++) {
+            $attrs[$result[0][$i]] = array();
+        }
+        $result = @ldap_mod_del($this->_connection, $this->_dn, $attrs);
+        if ($result === false) {
+            Horde::logMessage(
+                sprintf('Unable to clear user\'s preferences: [%d] %s',
+                        @ldap_errno($this->_connection),
+                        @ldap_error($this->_connection)),
+                __FILE__, __LINE__, PEAR_LOG_ERR);
+        }
+
+        $this->cleanup(true);
+    }
+
+}
diff --git a/framework/Prefs/lib/Horde/Prefs/Session.php b/framework/Prefs/lib/Horde/Prefs/Session.php
new file mode 100644 (file)
index 0000000..6f953e5
--- /dev/null
@@ -0,0 +1,67 @@
+<?php
+/**
+ * Preferences storage implementation for PHP's session implementation.
+ *
+ * Copyright 1999-2009 The Horde Project (http://www.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>
+ * @package Horde_Prefs
+ */
+class Horde_Prefs_Session extends Horde_Prefs
+{
+    /**
+     * Retrieves the requested set of preferences from the current session.
+     *
+     * @param string $scope  Scope specifier.
+     */
+    protected function _retrieve($scope)
+    {
+        if (isset($_SESSION['horde_prefs'][$scope])) {
+            $this->_scopes[$scope] = $_SESSION['horde_prefs'][$scope];
+        }
+    }
+
+    /**
+     * Stores preferences in the current session.
+     */
+    public function store()
+    {
+        // Create and register the preferences array, if necessary.
+        if (!isset($_SESSION['horde_prefs'])) {
+            $_SESSION['horde_prefs'] = array();
+        }
+
+        // Copy the current preferences into the session variable.
+        foreach ($this->_scopes as $scope => $prefs) {
+            $pref_keys = array_keys($prefs);
+            foreach ($pref_keys as $pref_name) {
+                // Clean the pref since it was just saved.
+                $prefs[$pref_name]['m'] &= ~Horde_Prefs::DIRTY;
+            }
+
+            $_SESSION['horde_prefs'][$scope] = $prefs;
+        }
+    }
+
+    /**
+     * Perform cleanup operations.
+     *
+     * @param boolean $all  Cleanup all Horde preferences.
+     */
+    public function cleanup($all = false)
+    {
+        // Perform a Horde-wide cleanup?
+        if ($all) {
+            unset($_SESSION['horde_prefs']);
+        } else {
+            unset($_SESSION['horde_prefs'][$this->_scope]);
+            $_SESSION['horde_prefs']['_filled'] = false;
+        }
+
+        parent::cleanup($all);
+    }
+
+}
diff --git a/framework/Prefs/lib/Horde/Prefs/Sql.php b/framework/Prefs/lib/Horde/Prefs/Sql.php
new file mode 100644 (file)
index 0000000..a9a298f
--- /dev/null
@@ -0,0 +1,390 @@
+<?php
+/**
+ * Preferences storage implementation for PHP's PEAR database
+ * abstraction layer.
+ *
+ * Required parameters:
+ * <pre>
+ * 'charset' - The database's internal charset.
+ * 'phptype' - The database type (ie. 'pgsql', 'mysql', etc.).
+ * </pre>
+ *
+ * Optional parameters:
+ * <pre>
+ * 'table' - The name of the preferences table in 'database'.
+ *           DEFAULT: 'horde_prefs'
+ * </pre>
+ *
+ * Required by some database implementations:
+ * <pre>
+ * 'database' - The name of the database.
+ * 'hostspec' - The hostname of the database server.
+ * 'options' - Additional options to pass to the database.
+ * 'password' - The password associated with 'username'.
+ * 'port' - The port on which to connect to the database.
+ * 'protocol' - The communication protocol ('tcp', 'unix', etc.).
+ * 'tty' - The TTY on which to connect to the database.
+ * 'username' - The username with which to connect to the database.
+ * </pre>
+ *
+ * Optional values when using separate reading and writing servers, for
+ * example in replication settings:
+ * <pre>
+ * 'read' - Array containing the parameters which are different for the read
+ *          database connection, currently supported only 'hostspec' and
+ *          'port' parameters.
+ * 'splitread' - (boolean) Whether to implement the separation or not.
+ * </pre>
+ *
+ * The table structure for the Prefs system is in
+ * scripts/sql/horde_prefs.sql.
+ *
+ * Copyright 1999-2009 The Horde Project (http://www.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>
+ * @category Horde
+ * @package  Horde_Prefs
+ */
+class Horde_Prefs_Sql extends Horde_Prefs
+{
+    /**
+     * Hash containing connection parameters.
+     *
+     * @var array
+     */
+    protected $_params = array();
+
+    /**
+     * Handle for the current database connection.
+     *
+     * @var DB
+     */
+    protected $_db;
+
+    /**
+     * Handle for the current database connection, used for
+     * writing. Defaults to the same handle as $_db if a separate
+     * write database is not configured.
+     *
+     * @var DB
+     */
+    protected $_write_db;
+
+    /**
+     * Boolean indicating whether or not we're connected to the SQL server.
+     *
+     * @var boolean
+     */
+    protected $_connected = false;
+
+    /**
+     * Returns the charset used by the concrete preference backend.
+     *
+     * @return string  The preference backend's charset.
+     */
+    public function getCharset()
+    {
+        return $this->_params['charset'];
+    }
+
+    /**
+     * Retrieves the requested set of preferences from the user's database
+     * entry.
+     *
+     * @param string $scope  Scope specifier.
+     */
+    protected function _retrieve($scope)
+    {
+        try {
+            $this->_connect();
+        } catch (Horde_Exception $e) {
+            if (empty($_SESSION['prefs_cache']['unavailable'])) {
+                $_SESSION['prefs_cache']['unavailable'] = true;
+                if (isset($GLOBALS['notification'])) {
+                    $GLOBALS['notification']->push(_("The preferences backend is currently unavailable and your preferences have not been loaded. You may continue to use the system with default settings."));
+                }
+            }
+            return;
+        }
+
+        $query = 'SELECT pref_scope, pref_name, pref_value FROM ' .
+            $this->_params['table'] . ' ' .
+            'WHERE pref_uid = ? AND pref_scope = ?';
+
+        $values = array($this->_user, $scope);
+
+        Horde::logMessage('SQL Query by Horde_Prefs_Sql::retrieve(): ' . $query . ', values: ' . implode(', ', $values), __FILE__, __LINE__, PEAR_LOG_DEBUG);
+
+        $result = $this->_db->query($query, $values);
+        if ($result instanceof PEAR_Error) {
+            Horde::logMessage('No preferences were retrieved.', __FILE__, __LINE__, PEAR_LOG_DEBUG);
+            return;
+        }
+
+        $row = $result->fetchRow(DB_FETCHMODE_ASSOC);
+        if ($row instanceof PEAR_Error) {
+            Horde::logMessage($row, __FILE__, __LINE__, PEAR_LOG_ERR);
+            return;
+        }
+
+        while ($row && !($row instanceof PEAR_Error)) {
+            $name = trim($row['pref_name']);
+
+            switch ($this->_db->phptype) {
+            case 'pgsql':
+                $row['pref_value'] = pg_unescape_bytea(stripslashes($row['pref_value']));
+                break;
+            }
+
+            if (isset($this->_scopes[$scope][$name])) {
+                $this->_scopes[$scope][$name]['v'] = $row['pref_value'];
+                $this->_scopes[$scope][$name]['m'] &= ~self::PREFS_DEFAULT;
+            } else {
+                // This is a shared preference.
+                $this->_scopes[$scope][$name] = array('v' => $row['pref_value'],
+                                                      'm' => 0,
+                                                      'd' => null);
+            }
+
+            $row = $result->fetchRow(DB_FETCHMODE_ASSOC);
+        }
+    }
+
+    /**
+     * Stores preferences to the SQL server.
+     *
+     * @throws Horde_Exception
+     */
+    public function store()
+    {
+        // Get the list of preferences that have changed. If there are
+        // none, no need to hit the backend.
+        $dirty_prefs = $this->_dirtyPrefs();
+        if (!$dirty_prefs) {
+            return;
+        }
+
+        $this->_connect();
+
+        // For each preference, check for an existing table row and
+        // update it if it's there, or create a new one if it's not.
+        foreach ($dirty_prefs as $scope => $prefs) {
+            foreach ($prefs as $name => $pref) {
+                // Don't store locked preferences.
+                if ($this->_scopes[$scope][$name]['m'] & self::LOCKED) {
+                    continue;
+                }
+
+                $values = array($this->_user, $name, $scope);
+
+                // Does a row already exist for this preference?
+                $query = 'SELECT 1 FROM ' . $this->_params['table'] .
+                    ' WHERE pref_uid = ? AND pref_name = ?' .
+                    ' AND pref_scope = ?';
+                Horde::logMessage('SQL Query by Horde_Prefs_Sql::store(): ' . $query . ', values: ' . implode(', ', $values), __FILE__, __LINE__, PEAR_LOG_DEBUG);
+
+                $check = $this->_write_db->getOne($query, $values);
+                if ($check instanceof PEAR_Error) {
+                    Horde::logMessage('Failed checking prefs for ' . $this->_user . ': ' . $check->getMessage(), __FILE__, __LINE__, PEAR_LOG_ERR);
+                    return;
+                }
+
+                $value = (string) (isset($pref['v']) ? $pref['v'] : null);
+
+                switch ($this->_db->phptype) {
+                case 'pgsql':
+                    $value = pg_escape_bytea($value);
+                    break;
+                }
+
+                if (!empty($check)) {
+                    // Update the existing row.
+                    $query = 'UPDATE ' . $this->_params['table'] .
+                        ' SET pref_value = ?' .
+                        ' WHERE pref_uid = ?' .
+                        ' AND pref_name = ?' .
+                        ' AND pref_scope = ?';
+
+                    $values = array($value,
+                                    $this->_user,
+                                    $name,
+                                    $scope);
+                } else {
+                    // Insert a new row.
+                    $query  = 'INSERT INTO ' . $this->_params['table'] . ' ' .
+                        '(pref_uid, pref_scope, pref_name, pref_value) VALUES' .
+                        '(?, ?, ?, ?)';
+
+                    $values = array($this->_user,
+                                    $scope,
+                                    $name,
+                                    $value);
+                }
+
+                Horde::logMessage('SQL Query by Horde_Prefs_Sql::store(): ' . $query . ', values: ' . implode(', ', $values), __FILE__, __LINE__, PEAR_LOG_DEBUG);
+
+                $result = $this->_write_db->query($query, $values);
+                if ($result instanceof PEAR_Error) {
+                    Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR);
+                    return;
+                }
+
+                // Clean the pref since it was just saved.
+                $this->_scopes[$scope][$name]['m'] &= ~self::DIRTY;
+            }
+
+            // Update the cache for this scope.
+            $this->_cacheUpdate($scope, array_keys($prefs));
+        }
+    }
+
+    /**
+     * Clears all preferences from the backend.
+     *
+     * @throws Horde_Exception
+     */
+    public function clear()
+    {
+        $this->_connect();
+
+        // Build the SQL query.
+        $query = 'DELETE FROM ' . $this->_params['table'] .
+            ' WHERE pref_uid = ?';
+
+        $values = array($this->_user);
+
+        Horde::logMessage('SQL Query by Horde_Prefs_Sql::clear():' . $query . ', values: ' . implode(', ', $values), __FILE__, __LINE__, PEAR_LOG_DEBUG);
+
+        // Execute the query.
+        $this->_write_db->query($query, $values);
+
+        // Cleanup.
+        parent::clear();
+    }
+
+    /**
+     * Converts a value from the driver's charset to the specified charset.
+     *
+     * @param mixed $value     A value to convert.
+     * @param string $charset  The charset to convert to.
+     *
+     * @return mixed  The converted value.
+     */
+    public function convertFromDriver($value, $charset)
+    {
+        static $converted = array();
+
+        if (is_array($value)) {
+            return Horde_String::convertCharset($value, $this->_params['charset'], $charset);
+        }
+
+        if (is_bool($value)) {
+            return $value;
+        }
+
+        if (!isset($converted[$charset][$value])) {
+            $converted[$charset][$value] = Horde_String::convertCharset($value, $this->_params['charset'], $charset);
+        }
+
+        return $converted[$charset][$value];
+    }
+
+    /**
+     * Converts a value from the specified charset to the driver's charset.
+     *
+     * @param mixed $value  A value to convert.
+     * @param string $charset  The charset to convert from.
+     *
+     * @return mixed  The converted value.
+     */
+    public function convertToDriver($value, $charset)
+    {
+        return Horde_String::convertCharset($value, $charset, $this->_params['charset']);
+    }
+
+    /**
+     * Attempts to open a persistent connection to the SQL server.
+     *
+     * @throws Horde_Exception
+     */
+    protected function _connect()
+    {
+        if ($this->_connected) {
+            return;
+        }
+
+        Horde::assertDriverConfig($this->_params, 'prefs',
+            array('phptype', 'charset'),
+            'preferences SQL');
+
+        if (!isset($this->_params['database'])) {
+            $this->_params['database'] = '';
+        }
+        if (!isset($this->_params['username'])) {
+            $this->_params['username'] = '';
+        }
+        if (!isset($this->_params['password'])) {
+            $this->_params['password'] = '';
+        }
+        if (!isset($this->_params['hostspec'])) {
+            $this->_params['hostspec'] = '';
+        }
+        if (!isset($this->_params['table'])) {
+            $this->_params['table'] = 'horde_prefs';
+        }
+
+        // Connect to the SQL server using the supplied parameters.
+        $this->_write_db = DB::connect($this->_params,
+                                       array('persistent' => !empty($this->_params['persistent']),
+                                             'ssl' => !empty($this->_params['ssl'])));
+        if ($this->_write_db instanceof PEAR_Error) {
+            Horde::logMessage($this->_write_db, __FILE__, __LINE__, PEAR_LOG_ERR);
+            throw new Horde_Exception($this->_write_db);
+        }
+
+        // Set DB portability options.
+        switch ($this->_write_db->phptype) {
+        case 'mssql':
+            $this->_write_db->setOption('portability', DB_PORTABILITY_LOWERCASE | DB_PORTABILITY_ERRORS | DB_PORTABILITY_RTRIM);
+            break;
+
+        default:
+            $this->_write_db->setOption('portability', DB_PORTABILITY_LOWERCASE | DB_PORTABILITY_ERRORS);
+            break;
+        }
+
+        // Check if we need to set up the read DB connection
+        // seperately.
+        if (!empty($this->_params['splitread'])) {
+            $params = array_merge($this->_params, $this->_params['read']);
+            $this->_db = DB::connect($params,
+                                     array('persistent' => !empty($params['persistent']),
+                                           'ssl' => !empty($params['ssl'])));
+            if ($this->_db instanceof PEAR_Error) {
+                Horde::logMessage($this->_db, __FILE__, __LINE__, PEAR_LOG_ERR);
+                throw new Horde_Exception($this->_db);
+            }
+
+            // Set DB portability options.
+            switch ($this->_db->phptype) {
+            case 'mssql':
+                $this->_db->setOption('portability', DB_PORTABILITY_LOWERCASE | DB_PORTABILITY_ERRORS | DB_PORTABILITY_RTRIM);
+                break;
+
+            default:
+                $this->_db->setOption('portability', DB_PORTABILITY_LOWERCASE | DB_PORTABILITY_ERRORS);
+                break;
+            }
+
+        } else {
+            // Default to the same DB handle for reads.
+            $this->_db = $this->_write_db;
+        }
+
+        $this->_connected = true;
+    }
+
+}
diff --git a/framework/Prefs/lib/Horde/Prefs/Ui.php b/framework/Prefs/lib/Horde/Prefs/Ui.php
new file mode 100644 (file)
index 0000000..12f7cbf
--- /dev/null
@@ -0,0 +1,415 @@
+<?php
+/**
+ * Class for auto-generating the preferences user interface and
+ * processing the forms.
+ *
+ * Set $_SESSION['horde_prefs']['nomenu'] to true to suppress output of the
+ * Horde_Menu on the options pages.
+ *
+ * For 'special' group types, set 'prefsui_no_save' to suppress printing of
+ * the "Save Changes" and "Undo Changes" buttons.
+ *
+ * Copyright 2001-2009 The Horde Project (http://www.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   Chuck Hagenbuch <chuck@horde.org>
+ * @category Horde
+ * @package  Horde_Prefs
+ */
+class Horde_Prefs_Ui
+{
+    /**
+     * Cache for groupIsEditable().
+     *
+     * @var array
+     */
+    static protected $_results = array();
+
+    /**
+     * Determine whether or not a preferences group is editable.
+     *
+     * @param string $group  The preferences group to check.
+     *
+     * @return boolean  Whether or not the group is editable.
+     */
+    static public function groupIsEditable($group)
+    {
+        global $prefs, $prefGroups;
+
+        if (!isset(self::$_results[$group])) {
+            if (!empty($prefGroups[$group]['url'])) {
+                self::$_results[$group] = true;
+            } else {
+                self::$_results[$group] = false;
+                if (isset($prefGroups[$group]['members'])) {
+                    foreach ($prefGroups[$group]['members'] as $pref) {
+                        if (!$prefs->isLocked($pref)) {
+                            self::$_results[$group] = true;
+                            break;
+                        }
+                    }
+                }
+            }
+        }
+
+        return self::$_results[$group];
+    }
+
+    /**
+     * Handle a preferences form submission if there is one, updating
+     * any preferences which have been changed.
+     *
+     * @param string $group  The preferences group that was edited.
+     * @param object $save   The object where the changed values are
+     *                       saved. Must implement setValue(string, string).
+     *
+     * @return boolean  Whether preferences have been updated.
+     */
+    static public function handleForm(&$group, &$save)
+    {
+        global $app, $prefs, $prefGroups, $_prefs, $registry;
+
+        $updated = false;
+
+        $notification = Horde_Notification::singleton();
+
+        /* Run through the action handlers */
+        if (Horde_Util::getPost('actionID') == 'update_prefs') {
+            if (isset($group) && self::groupIsEditable($group)) {
+                $updated = false;
+
+                foreach ($prefGroups[$group]['members'] as $pref) {
+                    if (!$prefs->isLocked($pref) ||
+                        ($_prefs[$pref]['type'] == 'special')) {
+                        switch ($_prefs[$pref]['type']) {
+
+                        /* These either aren't set or are set in other
+                         * parts of the UI. */
+                        case 'implicit':
+                        case 'link':
+                            break;
+
+                        case 'select':
+                        case 'text':
+                        case 'textarea':
+                        case 'password':
+                            $updated = $updated | $save->setValue($pref, Horde_Util::getPost($pref));
+                            break;
+
+                        case 'enum':
+                            $val = Horde_Util::getPost($pref);
+                            if (isset($_prefs[$pref]['enum'][$val])) {
+                                $updated = $updated | $save->setValue($pref, $val);
+                            } else {
+                                $notification->push(_("An illegal value was specified."), 'horde.error');
+                            }
+                            break;
+
+                        case 'multienum':
+                            $vals = Horde_Util::getPost($pref);
+                            $set = array();
+                            $invalid = false;
+                            if (is_array($vals)) {
+                                foreach ($vals as $val) {
+                                    if (isset($_prefs[$pref]['enum'][$val])) {
+                                        $set[] = $val;
+                                    } else {
+                                        $invalid = true;
+                                        continue;
+                                    }
+                                }
+                            }
+
+                            if ($invalid) {
+                                $notification->push(_("An illegal value was specified."), 'horde.error');
+                            } else {
+                                $updated = $updated | $save->setValue($pref, @serialize($set));
+                            }
+                            break;
+
+                        case 'number':
+                            $num = Horde_Util::getPost($pref);
+                            if ((string)(double)$num !== $num) {
+                                $notification->push(_("This value must be a number."), 'horde.error');
+                            } elseif (empty($num)) {
+                                $notification->push(_("This number must be at least one."), 'horde.error');
+                            } else {
+                                $updated = $updated | $save->setValue($pref, $num);
+                            }
+                            break;
+
+                        case 'checkbox':
+                            $val = Horde_Util::getPost($pref);
+                            $updated = $updated | $save->setValue($pref, isset($val) ? 1 : 0);
+                            break;
+
+                        case 'alarm':
+                            $methods = Horde_Alarm::notificationMethods();
+                            $value = array();
+                            foreach (Horde_Util::getPost($pref, array()) as $method) {
+                                $value[$method] = array();
+                                if (!empty($methods[$method])) {
+                                    foreach (array_keys($methods[$method]) as $param) {
+                                        $value[$method][$param] = Horde_Util::getPost($pref . '_' . $param, '');
+                                        if (is_array($methods[$method][$param]) &&
+                                            $methods[$method][$param]['required'] &&
+                                            $value[$method][$param] === '') {
+                                            $notification->push(sprintf(_("You must provide a setting for \"%s\"."), $methods[$method][$param]['desc']), 'horde.error');
+                                            $updated = false;
+                                            break 3;
+                                        }
+                                    }
+                                }
+                            }
+                            $updated = $updated | $save->setValue($pref, serialize($value));
+                            break;
+
+                        case 'special':
+                            /* Code for special elements written specifically
+                             * for each application. */
+                            if ($registry->hasAppMethod($app, 'prefsHandle')) {
+                                $updated = $updated | $registry->callAppMethod($app, 'prefsHandle', array('args' => array($pref, $updated)));
+                            }
+                            break;
+                        }
+                    }
+                }
+
+                if (is_callable(array($save, 'verify'))) {
+                    $result = $save->verify();
+                    if ($result instanceof PEAR_Error) {
+                        $notification->push($result, 'horde.error');
+                        $updated = false;
+                    }
+                }
+
+                if ($updated) {
+                    if ($registry->hasAppMethod($app, 'prefsCallback')) {
+                        $registry->callAppMethod($app, 'prefsCallback');
+                    }
+                    if ($prefs instanceof Horde_Prefs_Session) {
+                        $notification->push(_("Your options have been updated for the duration of this session."), 'horde.success');
+                    } else {
+                        $notification->push(_("Your options have been updated."), 'horde.success');
+                    }
+                    $group = null;
+                }
+            }
+        }
+
+        return $updated;
+    }
+
+    /**
+     * Generate the UI for the preferences interface, either for a
+     * specific group, or the group selection interface.
+     *
+     * @param string $group   The group to generate the UI for.
+     * @param boolean $chunk  Whether to only return the body part.
+     */
+    static public function generateUI($group = null, $chunk = false)
+    {
+        global $browser, $conf, $prefs, $prefGroups, $_prefs, $registry, $app;
+
+        $notification = Horde_Notification::singleton();
+
+        /* Check if any options are actually available. */
+        if (is_null($prefGroups)) {
+            $notification->push(_("There are no options available."), 'horde.message');
+        }
+
+        /* Assign variables to hold select lists. */
+        if (!$prefs->isLocked('language')) {
+            $GLOBALS['language_options'] = Horde_Nls::$config['languages'];
+            array_unshift($GLOBALS['language_options'], _("Default"));
+        }
+
+        $columns = array();
+        $in_group = (!empty($group) && self::groupIsEditable($group) && !empty($prefGroups[$group]['members']));
+
+        /* We need to do this check up here because it is possible that
+         * we will generate a notification object, which is handled by
+         * generateHeader. */
+        if (!$in_group && is_array($prefGroups)) {
+            foreach ($prefGroups as $key => $val) {
+                if (self::groupIsEditable($key)) {
+                    $col = $val['column'];
+                    unset($val['column']);
+                    $columns[$col][$key] = $val;
+                }
+            }
+            if (!count($columns)) {
+                $notification->push(_("There are no options available."), 'horde.message');
+            }
+        }
+
+        self::generateHeader($group, $chunk);
+
+        if ($in_group) {
+            foreach ($prefGroups[$group]['members'] as $pref) {
+                if (!$prefs->isLocked($pref)) {
+                    /* Get the help link. */
+                    $helplink = empty($_prefs[$pref]['help'])
+                        ? null
+                        : Horde_Help::link(!empty($_prefs[$pref]['shared']) ? 'horde' : $registry->getApp(), $_prefs[$pref]['help']);
+
+                    switch ($_prefs[$pref]['type']) {
+                    case 'implicit':
+                        break;
+
+                    case 'special':
+                        require $registry->get('templates', empty($_prefs[$pref]['shared']) ? $registry->getApp() : 'horde') . '/prefs/' . $pref . '.inc';
+                        break;
+
+                    default:
+                        require $registry->get('templates', 'horde') . '/prefs/' . $_prefs[$pref]['type'] . '.inc';
+                        break;
+                    }
+                }
+            }
+            require $registry->get('templates', 'horde') . '/prefs/end.inc';
+        } elseif (count($columns)) {
+            $span = round(100 / count($columns));
+            require $registry->get('templates', 'horde') . '/prefs/overview.inc';
+        }
+    }
+
+    /**
+     * Generates the the full header of a preference screen including
+     * menu and navigation bars.
+     *
+     * @param string $group   The group to generate the header for.
+     * @param boolean $chunk  Whether to only return the body part.
+     */
+    static public function generateHeader($group = null, $chunk = false)
+    {
+        global $registry, $prefGroups, $app, $perms, $prefs;
+
+        $notification = Horde_Notification::singleton();
+
+        $title = _("User Options");
+        if ($group == 'identities' && !$prefs->isLocked('default_identity')) {
+            $notification->push('newChoice()', 'javascript');
+        }
+        $GLOBALS['bodyId'] = 'services_prefs';
+        if (!$chunk) {
+            require $registry->get('templates', $app) . '/common-header.inc';
+
+            if (empty($_SESSION['horde_prefs']['nomenu'])) {
+                if ($registry->hasAppMethod($app, 'prefsMenu')) {
+                    $menu = $registry->callAppMethod($app, 'prefsMenu');
+                }
+                require $registry->get('templates', 'horde') . '/menu/menu.inc';
+            }
+
+            $notification->notify(array('listeners' => 'status'));
+        }
+
+        /* Get list of accessible applications. */
+        $apps = array();
+        foreach ($registry->applications as $application => $params) {
+            // Make sure the app is installed and has a prefs file.
+            if (!file_exists($registry->get('fileroot', $application) . '/config/prefs.php')) {
+                continue;
+            }
+
+            if ($params['status'] == 'heading' ||
+                $params['status'] == 'block') {
+                continue;
+            }
+
+            /* Check if the current user has permisson to see this
+             * application, and if the application is active.
+             * Administrators always see all applications. */
+            if ((Horde_Auth::isAdmin() && $params['status'] != 'inactive') ||
+                ($registry->hasPermission($application) &&
+                 ($params['status'] == 'active' || $params['status'] == 'notoolbar'))) {
+                $apps[$application] = _($params['name']);
+            }
+        }
+        asort($apps);
+
+        /* Show the current application and a form for switching
+         * applications. */
+        require $registry->get('templates', 'horde') . '/prefs/app.inc';
+
+        /* If there's only one prefGroup, just show it. */
+        if (empty($group) && count($prefGroups) == 1) {
+            $group = array_keys($prefGroups);
+            $group = array_pop($group);
+        }
+
+        if (!empty($group) && self::groupIsEditable($group)) {
+            require $registry->get('templates', 'horde') . '/prefs/begin.inc';
+        }
+    }
+
+    /**
+     * Generate the content of the title bar navigation cell (previous | next
+     * option group).
+     *
+     * @param string $group  Current option group.
+     */
+    static public function generateNavigationCell($group)
+    {
+        global $prefGroups, $registry, $app;
+
+        // Search for previous and next groups.
+        $first = $last = $next = $previous = null;
+        $finish = $found = false;
+
+        foreach ($prefGroups as $pgroup => $gval) {
+            if (self::groupIsEditable($pgroup)) {
+                if (!$first) {
+                    $first = $pgroup;
+                }
+                if (!$found) {
+                    if ($pgroup == $group) {
+                        $previous = $last;
+                        $found = true;
+                    }
+                } elseif (!$finish) {
+                    $finish = true;
+                    $next = $pgroup;
+                }
+                $last = $pgroup;
+            }
+        }
+
+        if (!$previous) {
+            $previous = $last;
+        }
+
+        if (!$next) {
+            $next = $first;
+        }
+
+        /* Don't loop if there's only one group. */
+        if ($next == $previous) {
+            return;
+        }
+
+        echo '<ul><li>' .
+             Horde::link(Horde_Util::addParameter(Horde::url($registry->get('webroot', 'horde') . '/services/prefs.php'), array('app' => $app, 'group' => $previous), _("Previous options"))) .
+             '&lt;&lt; ' . $prefGroups[$previous]['label'] .
+             '</a>&nbsp;|&nbsp;' .
+             Horde::link(Horde_Util::addParameter(Horde::url($registry->get('webroot', 'horde') . '/services/prefs.php'), array('app' => $app, 'group' => $next), _("Next options"))) .
+             $prefGroups[$next]['label'] . ' &gt;&gt;' .
+             '</a></li></ul>';
+    }
+
+    /**
+     * Get the default application to show preferences for. Defaults
+     * to 'horde'.
+     */
+    static public function getDefaultApp()
+    {
+        $applications = $GLOBALS['registry']->listApps(null, true, Horde_Perms::READ);
+        return isset($applications['horde'])
+            ? 'horde'
+            : array_shift($applications);
+    }
+
+}
diff --git a/framework/Prefs/package.xml b/framework/Prefs/package.xml
new file mode 100644 (file)
index 0000000..1cd2bc5
--- /dev/null
@@ -0,0 +1,150 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<package packagerversion="1.4.9" version="2.0" xmlns="http://pear.php.net/dtd/package-2.0" xmlns:tasks="http://pear.php.net/dtd/tasks-1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://pear.php.net/dtd/tasks-1.0
+http://pear.php.net/dtd/tasks-1.0.xsd
+http://pear.php.net/dtd/package-2.0
+http://pear.php.net/dtd/package-2.0.xsd">
+ <name>Prefs</name>
+ <channel>pear.horde.org</channel>
+ <summary>Horde Preferances API</summary>
+ <description>The Horde_Prefs:: package provides a common abstracted interface into the various preferences storage mediums.  It also includes all of the functions for retrieving, storing, and checking preference values.
+ </description>
+ <lead>
+  <name>Chuck Hagenbuch</name>
+  <user>chuck</user>
+  <email>chuck@horde.org</email>
+  <active>yes</active>
+ </lead>
+ <date>2009-11-21</date>
+ <version>
+  <release>0.1.0</release>
+  <api>0.1.0</api>
+ </version>
+ <stability>
+  <release>beta</release>
+  <api>beta</api>
+ </stability>
+ <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
+ <notes>* Initial Horde 4 package.
+ </notes>
+ <contents>
+  <dir name="/">
+   <dir name="lib">
+    <dir name="Horde">
+     <dir name="Prefs">
+      <file name="CategoryManager.php" role="php" />
+      <file name="Credentials.php" role="php" />
+      <file name="File.php" role="php" />
+      <file name="Identity.php" role="php" />
+      <file name="Imsp.php" role="php" />
+      <file name="Kolab.php" role="php" />
+      <file name="KolabImap.php" role="php" />
+      <file name="Ldap.php" role="php" />
+      <file name="Session.php" role="php" />
+      <file name="Sql.php" role="php" />
+      <file name="Ui.php" role="php" />
+     </dir> <!-- /lib/Horde/Prefs -->
+     <file name="Prefs.php" role="php" />
+    </dir> <!-- /lib/Horde -->
+   </dir> <!-- /lib -->
+   <dir name="test">
+    <dir name="Horde">
+     <dir name="Prefs">
+      <file name="bug_2838.phpt" role="test" />
+     </dir> <!-- /test/Horde/Prefs -->
+    </dir> <!-- /test/Horde -->
+   </dir> <!-- /test -->
+  </dir> <!-- / -->
+ </contents>
+ <dependencies>
+  <required>
+   <php>
+    <min>5.2.0</min>
+   </php>
+   <pearinstaller>
+    <min>1.7.0</min>
+   </pearinstaller>
+   <package>
+    <name>Core</name>
+    <channel>pear.horde.org</channel>
+   </package>
+   <package>
+    <name>Util</name>
+    <channel>pear.horde.org</channel>
+   </package>
+  </required>
+  <optional>
+   <extension>
+    <name>gettext</name>
+   </extension>
+   <package>
+    <name>Mime</name>
+    <channel>pear.horde.org</channel>
+   </package>
+  </optional>
+ </dependencies>
+ <phprelease>
+  <filelist>
+   <install name="lib/Horde/Prefs/CategoryManager.php" as="Horde/Prefs/CategoryManager.php" />
+   <install name="lib/Horde/Prefs/Credentials.php" as="Horde/Prefs/Credentials.php" />
+   <install name="lib/Horde/Prefs/File.php" as="Horde/Prefs/File.php" />
+   <install name="lib/Horde/Prefs/Identity.php" as="Horde/Prefs/Identity.php" />
+   <install name="lib/Horde/Prefs/Imsp.php" as="Horde/Prefs/Imsp.php" />
+   <install name="lib/Horde/Prefs/Kolab.php" as="Horde/Prefs/Kolab.php" />
+   <install name="lib/Horde/Prefs/KolabImap.php" as="Horde/Prefs/KolabImap.php" />
+   <install name="lib/Horde/Prefs/Ldap.php" as="Horde/Prefs/Ldap.php" />
+   <install name="lib/Horde/Prefs/Session.php" as="Horde/Prefs/Session.php" />
+   <install name="lib/Horde/Prefs/Sql.php" as="Horde/Prefs/Sql.php" />
+   <install name="lib/Horde/Prefs/Ui.php" as="Horde/Prefs/Ui.php" />
+   <install name="lib/Horde/Prefs.php" as="Horde/Prefs.php" />
+  </filelist>
+ </phprelease>
+ <changelog>
+  <release>
+   <date>2006-05-08</date>
+   <time>22:57:02</time>
+   <version>
+    <release>0.0.3</release>
+    <api>0.0.3</api>
+   </version>
+   <stability>
+    <release>alpha</release>
+    <api>alpha</api>
+   </stability>
+   <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
+   <notes>* Add 'nomenu' option to hide menu generation in Prefs_UI::.
+   * Data in postgres must be stored in a BYTEA field, not a TEXT field (Bug #8130).
+   * Converted to package.xml 2.0 for pear.horde.org
+   * Added files-based preferences backend (thomas.jarosch@intra2net.com, Request #6653)
+   </notes>
+  </release>
+  <release>
+   <version>
+    <release>0.0.2</release>
+    <api>0.0.2</api>
+   </version>
+   <stability>
+    <release>alpha</release>
+    <api>alpha</api>
+   </stability>
+   <date>2004-01-01</date>
+   <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
+   <notes>* Add failover functionality, if one of the drivers is not available will fall back to session-based preferences.
+* Add support for separate read and write DB servers for the sql driver.
+   </notes>
+  </release>
+  <release>
+   <version>
+    <release>0.0.1</release>
+    <api>0.0.1</api>
+   </version>
+   <stability>
+    <release>alpha</release>
+    <api>alpha</api>
+   </stability>
+   <date>2003-07-05</date>
+   <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
+   <notes>Initial release as a PEAR package
+   </notes>
+  </release>
+ </changelog>
+</package>
diff --git a/framework/Prefs/test/Horde/Prefs/bug_2838.phpt b/framework/Prefs/test/Horde/Prefs/bug_2838.phpt
new file mode 100644 (file)
index 0000000..b434a16
--- /dev/null
@@ -0,0 +1,22 @@
+--TEST--
+Test for Bug #2838, overwriting of preferences when multiple scopes are retrieved.
+--FILE--
+<?php
+
+define('HORDE_BASE', dirname(dirname(dirname(dirname(__FILE__)))));
+require_once HORDE_BASE . '/lib/core.php';
+
+$registry = Horde_Registry::singleton();
+
+$prefs = Horde_Prefs::factory('session', 'horde', 'testuser', 'testpw');
+$prefs->retrieve('imp');
+$prefs->setValue('last_login', 'test');
+echo $prefs->getValue('last_login') . "\n";
+
+$prefs->retrieve('ingo');
+echo $prefs->getValue('last_login') . "\n";
+
+?>
+--EXPECT--
+test
+test